{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<p><font size=-1 color=gray>\n",
    "&copy; Copyright 2018 IBM Corp. All Rights Reserved.\n",
    "<p>\n",
    "Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file\n",
    "except in compliance with the License. You may obtain a copy of the License at\n",
    "http://www.apache.org/licenses/LICENSE-2.0\n",
    "Unless required by applicable law or agreed to in writing, software distributed under the\n",
    "License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either\n",
    "express or implied. See the License for the specific language governing permissions and\n",
    "limitations under the License.\n",
    "</font></p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Analyze Clickstream Events\n",
    "\n",
    "This notebook uses the [Scala](https://www.scala-lang.org/) programming language\n",
    "to interact with IBM Db2 Event Stream. It demonstrates how to:\n",
    "\n",
    "* Connect to Event Store\n",
    "* Analyze clickstream data to gain insight into customer interests\n",
    "* Visualize the information with interactive Brunel charts"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Connect to IBM Db2 Event Store\n",
    "\n",
    "### Determine the IP address of your host\n",
    "\n",
    "Obtain the IP address of the host that you want to connect to by running the appropriate command for your operating system:\n",
    "\n",
    "* On Mac, run: `ifconfig`\n",
    "* On Windows, run: `ipconfig`\n",
    "* On Linux, run: `hostname -i`\n",
    "\n",
    "Edit the `HOST = \"XXX.XXX.XXX.XXX\"` value in the next cell to provide the IP address."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "// Set your host IP address\n",
    "val Host = \"XXX.XXX.XXX.XXX\"\n",
    "\n",
    "// Port will be 1100 for version 1.1.2 or later (5555 for version 1.1.1)\n",
    "val Port = \"1100\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Add Brunel integration\n",
    "Use cell magic to install the Brunel integration for Apache Toree (Scala)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Starting download from https://brunelvis.org/jar/spark-kernel-brunel-all-2.3.jar\n",
      "Finished download of spark-kernel-brunel-all-2.3.jar\n"
     ]
    }
   ],
   "source": [
    "%AddJar -magic https://brunelvis.org/jar/spark-kernel-brunel-all-2.3.jar -f"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Import Scala packages\n",
    "\n",
    "Import packages for Scala, Spark, and IBM Db2 Event Store."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "import sys.process._\n",
    "import java.io.File\n",
    "import scala.concurrent.{Await, Future}\n",
    "import scala.concurrent.duration.Duration\n",
    "import org.apache.log4j.{Level, LogManager, Logger}\n",
    "import org.apache.spark._\n",
    "import org.apache.spark.sql.expressions.Window\n",
    "import org.apache.spark.sql.functions._\n",
    "import org.apache.spark.sql.ibm.event.EventSession\n",
    "import org.apache.spark.sql.Row\n",
    "import org.apache.spark.sql.types._\n",
    "import com.ibm.event.catalog.TableSchema\n",
    "import com.ibm.event.common.ConfigurationReader\n",
    "import com.ibm.event.example.DataGenerator\n",
    "import com.ibm.event.oltp.EventContext\n",
    "import com.ibm.event.oltp.InsertResult"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Connect to Event Store"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "ConfigurationReader.setConnectionEndpoints(Host + \":\" + Port)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Load data from the Event Store table into a DataFrame"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+-----------+---------+----------+------------+-----------------+------+--------------------+-------+\n",
      "|    eventId|eventType| timestamp|   ipaddress|        sessionId|userId|             pageUrl|browser|\n",
      "+-----------+---------+----------+------------+-----------------+------+--------------------+-------+\n",
      "|20170522901| pageView|1496311260|169.34.56.78|y20170522a4499u21|ceaton|  /www.cybershop.com| Chrome|\n",
      "|20170522902| pageView|1496311320|169.34.56.78|y20170522a4499u21|ceaton|/estore?product_l...| Chrome|\n",
      "|20170522903| pageView|1496311440|169.34.56.78|y20170522a4499u21|ceaton|/estore?product_l...| Chrome|\n",
      "|20170522904| pageView|1496311500|169.34.56.78|y20170522a4499u21|ceaton|/estore?product_l...| Chrome|\n",
      "|20170522905| pageView|1496311560|169.34.56.78|y20170522a4499u21|ceaton|/estore?product_l...| Chrome|\n",
      "+-----------+---------+----------+------------+-----------------+------+--------------------+-------+\n",
      "only showing top 5 rows\n",
      "\n"
     ]
    }
   ],
   "source": [
    "val sqlContext = new EventSession(spark.sparkContext, \"TESTDB\")\n",
    "import sqlContext.implicits._\n",
    "\n",
    "val table = sqlContext.loadEventTable(\"ClickStreamTable\")\n",
    "table.registerTempTable(\"ClickStreamTable\")\n",
    "\n",
    "val clickStreamDF = sqlContext.sql(\"select * from ClickStreamTable\")\n",
    "clickStreamDF.show(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Prepare the clickstream data\n",
    "\n",
    "Use Spark SQL and Spark functions to build DataFrames with aggregated web metrics.\n",
    "\n",
    "### Calculate time spent on web pages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+-----------+---------+----------+------------+-----------------+------+--------------------+-------+----+\n",
      "|    eventId|eventType| timestamp|   ipaddress|        sessionId|userId|             pageUrl|browser|time|\n",
      "+-----------+---------+----------+------------+-----------------+------+--------------------+-------+----+\n",
      "|20170522901| pageView|1496311260|169.34.56.78|y20170522a4499u21|ceaton|  /www.cybershop.com| Chrome|  60|\n",
      "|20170522902| pageView|1496311320|169.34.56.78|y20170522a4499u21|ceaton|/estore?product_l...| Chrome| 120|\n",
      "|20170522903| pageView|1496311440|169.34.56.78|y20170522a4499u21|ceaton|/estore?product_l...| Chrome|  60|\n",
      "|20170522904| pageView|1496311500|169.34.56.78|y20170522a4499u21|ceaton|/estore?product_l...| Chrome|  60|\n",
      "|20170522905| pageView|1496311560|169.34.56.78|y20170522a4499u21|ceaton|/estore?product_l...| Chrome|  60|\n",
      "+-----------+---------+----------+------------+-----------------+------+--------------------+-------+----+\n",
      "only showing top 5 rows\n",
      "\n"
     ]
    }
   ],
   "source": [
    "val timestamp = clickStreamDF(\"timestamp\")\n",
    "val next_timestamp = lead(timestamp, 1).over(Window.orderBy(timestamp))\n",
    "\n",
    "// Calculate time on spent on web pages\n",
    "val clickStreamWithTimeDF = clickStreamDF.withColumn(\n",
    "  \"time\", next_timestamp.cast(LongType) - timestamp.cast(LongType))\n",
    "clickStreamWithTimeDF.show(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Calculate aggregated page hits and time spent"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+-----------------------------------------------------------------------------+---------+----------+\n",
      "|pageURL                                                                      |page_hits|total_time|\n",
      "+-----------------------------------------------------------------------------+---------+----------+\n",
      "|/www.cybershop.com                                                           |9        |600       |\n",
      "|/estore?product_line=smartphones&action=catalog                              |13       |1260      |\n",
      "|/estore?product_line=smartphones&product=A-phone&action=details              |7        |540       |\n",
      "|/estore?product_line=smartphones&product=A-phone&feature=color&action=details|5        |660       |\n",
      "|/estore?product_line=smartphones&product=S-phone&action=details              |5        |600       |\n",
      "+-----------------------------------------------------------------------------+---------+----------+\n",
      "only showing top 5 rows\n",
      "\n"
     ]
    }
   ],
   "source": [
    "val sqlContext = new org.apache.spark.sql.SQLContext(sc)\n",
    "\n",
    "clickStreamWithTimeDF.registerTempTable(\"tempData\")\n",
    "val clickStreamWithDateTimeDF = sqlContext.sql(\n",
    "  \"select eventId, eventType, cast(from_unixtime(timestamp) as date), \" +\n",
    "    \"ipaddress, sessionId, userId, pageUrl, browser, time \" +\n",
    "    \"from tempData\").withColumnRenamed(\n",
    "      \"CAST(from_unixtime(CAST(timestamp AS BIGINT), yyyy-MM-dd HH:mm:ss) AS DATE)\",\n",
    "      \"date\")\n",
    "// clickStreamWithDateTimeDF.show(5)\n",
    "\n",
    "clickStreamWithDateTimeDF.registerTempTable(\"ClickData\")\n",
    "val clicksDF = sqlContext.sql(\n",
    "  \"select pageURL, count(*) as page_hits, sum(time) as total_time \" +\n",
    "  \"from ClickData where eventType='pageView' group by pageURL\")\n",
    "clicksDF.show(5,false)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+--------------------+---------+----------+\n",
      "|             pageURL|page_hits|total_time|\n",
      "+--------------------+---------+----------+\n",
      "|  /www.cybershop.com|        9|       600|\n",
      "|/estore?product_l...|       13|      1260|\n",
      "|/estore?product_l...|        7|       540|\n",
      "|/estore?product_l...|        5|       660|\n",
      "|/estore?product_l...|        5|       600|\n",
      "+--------------------+---------+----------+\n",
      "only showing top 5 rows\n",
      "\n"
     ]
    }
   ],
   "source": [
    "clicksDF.registerTempTable(\"WebMetricsData\")\n",
    "val webMetricsDF = sqlContext.sql(\"select * from WebMetricsData\")\n",
    "webMetricsDF.show(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Calculate aggregated web metrics by product line, product, and feature"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+------------+\n",
      "|product_line|\n",
      "+------------+\n",
      "|  videogames|\n",
      "|  videogames|\n",
      "| smartphones|\n",
      "| smartphones|\n",
      "| smartphones|\n",
      "+------------+\n",
      "only showing top 5 rows\n",
      "\n",
      "+------------+-------+-------+---------+---------+----------+\n",
      "|product_line| action|product|  feature|page_hits|total_time|\n",
      "+------------+-------+-------+---------+---------+----------+\n",
      "|  videogames|details| W-game|         |        1|       120|\n",
      "|  videogames|catalog|       |         |        6|      4680|\n",
      "| smartphones|details|A-phone|processor|        2|       120|\n",
      "| smartphones|details|A-phone|         |        7|       540|\n",
      "| smartphones|details|A-phone|    color|        5|       660|\n",
      "+------------+-------+-------+---------+---------+----------+\n",
      "only showing top 5 rows\n",
      "\n"
     ]
    }
   ],
   "source": [
    "clicksDF.registerTempTable(\"WebMetricsDataTest\")\n",
    "val metricsQuery = \"\"\"\n",
    "  select\n",
    "    parse_URL(pageURL,'QUERY','product_line') as product_line, \n",
    "    Coalesce(parse_URL(pageURL,'QUERY','action'),'') as action,\n",
    "    Coalesce(parse_URL(pageURL,'QUERY','product'),'') as product, \n",
    "    Coalesce(parse_URL(pageURL,'QUERY','feature'),'') as feature, page_hits, total_time\n",
    "  from WebMetricsData\"\"\"\n",
    "val metricsQuery2 = \"\"\"\n",
    "  select\n",
    "    parse_URL(pageURL,'QUERY','product_line') as product_line, \n",
    "    parse_URL(pageURL,'QUERY','action') as action,\n",
    "    parse_URL(pageURL,'QUERY','product') as product, \n",
    "  from WebMetricsData\"\"\"\n",
    "val metricsQuery3 = \"\"\"\n",
    "  select parse_URL(pageURL,'QUERY','product_line') as product_line\n",
    "  from WebMetricsDataTest\"\"\"\n",
    "val webMetricsDF3 = sqlContext.sql(metricsQuery3).filter($\"product_line\".isNotNull).sort($\"product_line\".desc)\n",
    "webMetricsDF3.show(5)\n",
    "val webMetricsDF = sqlContext.sql(metricsQuery).filter($\"product_line\".isNotNull).sort($\"product_line\".desc)\n",
    "webMetricsDF.show(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Aggregated web metrics for all product lines"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+--------------+---------+----------+\n",
      "|  product_line|page_hits|total_time|\n",
      "+--------------+---------+----------+\n",
      "|   smartphones|       58|      6360|\n",
      "|     computers|       16|      3720|\n",
      "|    videogames|        7|      4800|\n",
      "|    appliances|        5|      2220|\n",
      "|   hometheater|        4|       840|\n",
      "|    headphones|        4|       960|\n",
      "|carelectronics|        2|       420|\n",
      "|       cameras|        2|       360|\n",
      "+--------------+---------+----------+\n",
      "\n"
     ]
    }
   ],
   "source": [
    "val productlineMetrics = webMetricsDF.\n",
    "  select(\"product_line\",\"page_hits\",\"total_time\").\n",
    "  groupBy(\"product_line\").agg(sum(\"page_hits\"), sum(\"total_time\")).\n",
    "  withColumnRenamed(\"sum(page_hits)\",\"page_hits\").\n",
    "  withColumnRenamed(\"sum(total_time)\",\"total_time\").\n",
    "  sort($\"page_hits\".desc)\n",
    "   \n",
    "productlineMetrics.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "collapsed": false,
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "         <link rel=\"stylesheet\" type=\"text/css\" href=\"https://brunelvis.org/js/brunel.2.3.css\" charset=\"utf-8\">\n",
       "         <link rel=\"stylesheet\" type=\"text/css\" href=\"https://brunelvis.org/js/sumoselect.css\" charset=\"utf-8\">\n",
       "         <style>  </style>\n",
       "         <div id=\"controlsId5d208eb0-4c9c-4ca9-9627-09bf9759cc4d\" class=\"brunel\"/>\n",
       "<svg id=\"visidee9cc519-cb99-41be-93da-c926b0428acd\" width=\"1000\" height=\"600\"></svg>\n",
       "\n",
       "<script>\n",
       "require.config({\n",
       "            waitSeconds: 60,\n",
       "            paths: {\n",
       "                'd3': '//cdnjs.cloudflare.com/ajax/libs/d3/4.2.1/d3.min',\n",
       "                'topojson' : '//cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min',\n",
       "                'brunel' : 'https://brunelvis.org/js/brunel.2.3.min',\n",
       "                'brunelControls' : 'https://brunelvis.org/js/brunel.controls.2.3.min'\n",
       "            },\n",
       "\n",
       "            shim: {\n",
       "               'brunel' : {\n",
       "                    exports: 'BrunelD3',\n",
       "                    deps: ['d3', 'topojson'],\n",
       "                    init: function() {\n",
       "                       return {\n",
       "                         BrunelD3 : BrunelD3,\n",
       "                         BrunelData : BrunelData\n",
       "                      }\n",
       "                    }\n",
       "                },\n",
       "               'brunelControls' : {\n",
       "                    exports: 'BrunelEventHandlers',\n",
       "                    init: function() {\n",
       "                       return {\n",
       "                         BrunelEventHandlers: BrunelEventHandlers,\n",
       "                         BrunelJQueryControlFactory: BrunelJQueryControlFactory\n",
       "                      }\n",
       "                    }\n",
       "                }\n",
       "\n",
       "            }\n",
       "\n",
       "        });\n",
       "\n",
       "        require([\"d3\"], function(d3) {\n",
       "        require([\"brunel\", \"brunelControls\"], function(brunel, brunelControls) {\n",
       "\n",
       "            function  BrunelVis(visId) {\n",
       "  \"use strict\";                                                                       // strict mode\n",
       "  var datasets = [],                                      // array of datasets for the original data\n",
       "      pre = function(d, i) { return d },                         // default pre-process does nothing\n",
       "      post = function(d, i) { return d },                       // default post-process does nothing\n",
       "      transitionTime = 200,                                        // transition time for animations\n",
       "      charts = [],                                                       // the charts in the system\n",
       "      vis = d3.select('#' + visId).attr('class', 'brunel');                     // the SVG container\n",
       "\n",
       "  BrunelD3.addDefinitions(vis);                                   // ensure standard symbols present\n",
       "\n",
       "  // Define chart #1 in the visualization //////////////////////////////////////////////////////////\n",
       "\n",
       "  charts[0] = function(parentNode, filterRows) {\n",
       "    var geom = BrunelD3.geometry(parentNode || vis.node(), 0, 0, 0.5, 0.5, 22, 43, 93, 0),\n",
       "      elements = [];                                              // array of elements in this chart\n",
       "\n",
       "    // Define groups for the chart parts ///////////////////////////////////////////////////////////\n",
       "\n",
       "    var chart =  vis.append('g').attr('class', 'chart1')\n",
       "      .attr('transform','translate(' + geom.chart_left + ',' + geom.chart_top + ')');\n",
       "    var overlay = chart.append('g').attr('class', 'element').attr('class', 'overlay');\n",
       "    var zoom = d3.zoom().scaleExtent([1/3,3]);\n",
       "    var zoomNode = overlay.append('rect').attr('class', 'overlay')\n",
       "      .attr('x', geom.inner_left).attr('y', geom.inner_top)\n",
       "      .attr('width', geom.inner_rawWidth).attr('height', geom.inner_rawHeight)\n",
       "      .style('cursor', 'move').call(zoom)\n",
       "      .node();\n",
       "    zoomNode.__zoom = d3.zoomIdentity;\n",
       "    chart.append('rect').attr('class', 'background').attr('width', geom.chart_right-geom.chart_left).attr('height', geom.chart_bottom-geom.chart_top);\n",
       "    var interior = chart.append('g').attr('class', 'interior zoomNone')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')')\n",
       "      .attr('clip-path', 'url(#clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart1_inner)');\n",
       "    interior.append('rect').attr('class', 'inner').attr('width', geom.inner_width).attr('height', geom.inner_height);\n",
       "    var gridGroup = interior.append('g').attr('class', 'grid');\n",
       "    var axes = chart.append('g').attr('class', 'axis')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart1_inner').append('rect')\n",
       "      .attr('x', 0).attr('y', 0)\n",
       "      .attr('width', geom.inner_rawWidth+1).attr('height', geom.inner_rawHeight+1);\n",
       "    chart.append('text').attr('class', 'title header').text('Page Views by Product Line').style('text-anchor', 'middle')\n",
       "      .attr('x','50%')\n",
       "      .attr('y',2).attr('dy','0.8em');\n",
       "\n",
       "    // Scales //////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    var scale_x = d3.scalePoint().padding(0.5)\n",
       "      .domain(['smartphones', 'computers', 'videogames', 'appliances', 'headphones', 'hometheater', 'cameras', 'carelectronics'])\n",
       "      .range([0, geom.inner_width]);\n",
       "    var scale_inner = d3.scaleLinear().domain([0,1])\n",
       "      .range([-0.5, 0.5]);\n",
       "    var scale_y = d3.scaleSqrt().domain([0, 60.000006])\n",
       "      .range([geom.inner_height, 0]);\n",
       "    var base_scales = [scale_x, scale_y];                           // untransformed original scales\n",
       "\n",
       "    // Axes ////////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    axes.append('g').attr('class', 'x axis')\n",
       "      .attr('transform','translate(0,' + geom.inner_rawHeight + ')')\n",
       "      .attr('clip-path', 'url(#clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart1_haxis)');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart1_haxis').append('polyline')\n",
       "      .attr('points', '-1,-1000, -1,-1 -5,5, -1000,5, -100,1000, 10000,1000 10000,-1000');\n",
       "    axes.select('g.axis.x').append('text').attr('class', 'title').text('product lines').style('text-anchor', 'middle')\n",
       "      .attr('x',geom.inner_rawWidth/2)\n",
       "      .attr('y', geom.inner_bottom - 2.0).attr('dy','-0.27em');\n",
       "    axes.append('g').attr('class', 'y axis')\n",
       "      .attr('clip-path', 'url(#clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart1_vaxis)');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart1_vaxis').append('polyline')\n",
       "      .attr('points', '-1000,-10000, 10000,-10000, 10000,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+5) + ', -1000,' + (geom.inner_rawHeight+5) );\n",
       "    axes.select('g.axis.y').append('text').attr('class', 'title').text('page views').style('text-anchor', 'middle')\n",
       "      .attr('x',-geom.inner_rawHeight/2)\n",
       "      .attr('y', 4-geom.inner_left).attr('dy', '0.7em').attr('transform', 'rotate(270)');\n",
       "\n",
       "    var axis_bottom = d3.axisBottom(scale_x).ticks(Math.min(10, Math.round(geom.inner_width / 124.5)));\n",
       "    var axis_left = d3.axisLeft(scale_y).ticks(Math.min(10, Math.round(geom.inner_width / 20)));\n",
       "\n",
       "    function buildAxes(time) {\n",
       "      axis_bottom.tickValues(BrunelD3.filterTicks(scale_x))\n",
       "      var axis_x = axes.select('g.axis.x');\n",
       "      BrunelD3.transition(axis_x, time).call(axis_bottom.scale(scale_x)).selectAll('.tick text')\n",
       "        .attr('transform', function() {\n",
       "          var v = this.getComputedTextLength() / Math.sqrt(2)/2;\n",
       "          return 'translate(-' + (v+6) + ',' + v + ') rotate(-45)'\n",
       "      });\n",
       "      var axis_y = axes.select('g.axis.y');\n",
       "      BrunelD3.transition(axis_y, time).call(axis_left.scale(scale_y));\n",
       "    }\n",
       "    zoom.on('zoom', function(t, time) {\n",
       "        t = t ||BrunelD3.restrictZoom(d3.event.transform, geom, this);\n",
       "        scale_y = t.rescaleY(base_scales[1]);\n",
       "        zoomNode.__zoom = t;\n",
       "        interior.attr('class', 'interior ' + BrunelD3.zoomLabel(t.k));;\n",
       "        build(time || -1);\n",
       "    });\n",
       "\n",
       "    // Define element #1 ///////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    elements[0] = function() {\n",
       "      var original, processed,                           // data sets passed in and then transformed\n",
       "        element, data,                                 // brunel element information and brunel data\n",
       "        selection, merged;                                      // d3 selection and merged selection\n",
       "      var elementGroup = interior.append('g').attr('class', 'element1'),\n",
       "        main = elementGroup.append('g').attr('class', 'main'),\n",
       "        labels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'labels').attr('aria-hidden', 'true'), elementGroup);\n",
       "\n",
       "      function makeData() {\n",
       "        original = datasets[0];\n",
       "        if (filterRows) original = original.retainRows(filterRows);\n",
       "        processed = pre(original, 0)\n",
       "          .sort('page_hits');\n",
       "        processed = post(processed, 0);\n",
       "        var f0 = processed.field('product_line'),\n",
       "          f1 = processed.field('page_hits'),\n",
       "          f2 = processed.field('#row'),\n",
       "          f3 = processed.field('#selection');\n",
       "        var keyFunc = function(d) { return f0.value(d) };\n",
       "        data = {\n",
       "          product_line: function(d) { return f0.value(d.row) },\n",
       "          page_hits:    function(d) { return f1.value(d.row) },\n",
       "          $row:         function(d) { return f2.value(d.row) },\n",
       "          $selection:   function(d) { return f3.value(d.row) },\n",
       "          product_line_f:function(d) { return f0.valueFormatted(d.row) },\n",
       "          page_hits_f:  function(d) { return f1.valueFormatted(d.row) },\n",
       "          $row_f:       function(d) { return f2.valueFormatted(d.row) },\n",
       "          $selection_f: function(d) { return f3.valueFormatted(d.row) },\n",
       "          _split:       function(d) { return f0.value(d.row) },\n",
       "          _key:         keyFunc,\n",
       "          _rows:        BrunelD3.makeRowsWithKeys(keyFunc, processed.rowCount())\n",
       "        };\n",
       "      }\n",
       "      // Aesthetic Functions\n",
       "      var scale_color = d3.scaleOrdinal()\n",
       "        .domain(['smartphones', 'computers', 'videogames', 'appliances', 'headphones', 'hometheater', 'cameras', 'carelectronics'])\n",
       "        .range([ '#347DAD', '#D43F58', '#F7D84A', '#31A461', '#A66A9C', '#FF954D', \n",
       "          '#A7978E', '#FFCA4D', '#F99EAF', '#B1C43B', '#7E64A2', '#FFB04D', '#CA5C7C', \n",
       "          '#DDBC8C', '#FFA28D', '#A5473D', '#8B6141', '#F57357', '#5C6B46']);\n",
       "      var color = function(d) { return scale_color(data.product_line(d)) };\n",
       "\n",
       "      // Build element from data ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "      function build(transitionMillis) {\n",
       "        element = elements[0];\n",
       "        var w = 0.9 * Math.abs(scale_x(scale_x.domain()[1]) - scale_x(scale_x.domain()[0]) );\n",
       "        var x = function(d) { return scale_x(data.product_line(d))};\n",
       "        var h = geom.default_point_size;\n",
       "        var y1 = scale_y.range()[0];\n",
       "        var y2 = function(d) { return scale_y(data.page_hits(d))};\n",
       "\n",
       "        // Define selection entry operations\n",
       "        function initialState(selection) {\n",
       "          selection\n",
       "            .attr('class', 'element bar filled')\n",
       "        }\n",
       "\n",
       "        // Define selection update operations on merged data\n",
       "        function updateState(selection) {\n",
       "          selection\n",
       "            .each(function(d) {\n",
       "              var width = w, left = x(d) - width/2, \n",
       "              c = y1, d = y2(d), top = Math.min(c,d), height = Math.max(1e-6, Math.abs(c-d));\n",
       "              this.r = {x:left, y:top, w:width, h:height};\n",
       "            })\n",
       "            .attr('x', function(d) { return this.r.x })\n",
       "            .attr('y', function(d) { return this.r.y })\n",
       "            .attr('width', function(d) { return this.r.w })\n",
       "            .attr('height', function(d) { return this.r.h })\n",
       "            .filter(BrunelD3.hasData)                     // following only performed for data items\n",
       "            .style('fill', color);\n",
       "        }\n",
       "\n",
       "        // Define labeling for the selection\n",
       "        function label(selection, transitionMillis) {\n",
       "\n",
       "          var tooltipLabeling  = {\n",
       "            index: -1, method: 'box', location: ['center', 'top'], inside: true, align: 'middle', pad: 0, dy: 0.7,\n",
       "            fit: true, granularity: 0,\n",
       "            content: function(d) {\n",
       "              return d.row == null ? null : '<span class=\"title\">product_line: </span>'\n",
       "\t\t\t+ '<span class=\"field\">' + data.product_line_f(d) + '</span>'\n",
       "\t\t\t+ '<br/>'\n",
       "\t\t\t+ '<span class=\"title\">page_hits: </span>'\n",
       "\t\t\t+ '<span class=\"field\">' + data.page_hits_f(d) + '</span>'\n",
       "            }\n",
       "          };\n",
       "          BrunelD3.addTooltip(selection, tooltipLabeling, geom);\n",
       "        }\n",
       "        // Create selections, set the initial state and transition updates\n",
       "        selection = main.selectAll('.element').data(data._rows, function(d) { return d.key });\n",
       "        var added = selection.enter().append('rect');\n",
       "        merged = selection.merge(added);\n",
       "        initialState(added);\n",
       "        selection.filter(BrunelD3.hasData)\n",
       "          .classed('selected', BrunelD3.isSelected(data))\n",
       "          .filter(BrunelD3.isSelected(data)).raise();\n",
       "        updateState(BrunelD3.transition(merged, transitionMillis));\n",
       "        label(merged, transitionMillis);\n",
       "\n",
       "        BrunelD3.transition(selection.exit(), transitionMillis/3)\n",
       "          .style('opacity', 0.5).each( function() {\n",
       "            this.remove(); BrunelD3.removeLabels(this); \n",
       "        });\n",
       "        chart.select('rect.overlay')                               // attach handlers to the overlay\n",
       "          .on('click.interact', function(d) {\n",
       "            var c = BrunelD3.closest(merged, 'xy', geom.inner_radius/4 );\n",
       "            BrunelD3.select(null, this, element, updateAll);\n",
       "          });\n",
       "        merged                                                     // attach handlers to the element\n",
       "          .on('click.interact', function(d) {BrunelD3.select(d, this, element, updateAll) });\n",
       "      }\n",
       "\n",
       "      return {\n",
       "        data:           function() { return processed },\n",
       "        original:       function() { return original },\n",
       "        internal:       function() { return data },\n",
       "        selection:      function() { return merged },\n",
       "        makeData:       makeData,\n",
       "        build:          build,\n",
       "        chart:          function() { return charts[0] },\n",
       "        group:          function() { return elementGroup },\n",
       "        fields: {\n",
       "          x:            ['product_line'],\n",
       "          y:            ['page_hits'],\n",
       "          key:          ['product_line'],\n",
       "          color:        ['product_line']\n",
       "        }\n",
       "      };\n",
       "    }();\n",
       "\n",
       "    function build(time, noData) {\n",
       "      var first = elements[0].data() == null;\n",
       "      if (first) time = 0;                                           // no transition for first call\n",
       "      buildAxes(time);\n",
       "      if ((first || time > -1) && !noData) {\n",
       "        elements[0].makeData();\n",
       "      }\n",
       "      elements[0].build(time);\n",
       "    }\n",
       "\n",
       "    // Expose the following components of the chart\n",
       "    return {\n",
       "      elements : elements,\n",
       "      interior : interior,\n",
       "      scales: {x:scale_x, y:scale_y},\n",
       "      zoom: function(params, time) {\n",
       "          if (params) zoom.on('zoom').call(zoomNode, params, time);\n",
       "          return d3.zoomTransform(zoomNode);\n",
       "      },\n",
       "      build : build\n",
       "    };\n",
       "    }();\n",
       "\n",
       "  // Define chart #2 in the visualization //////////////////////////////////////////////////////////\n",
       "\n",
       "  charts[1] = function(parentNode, filterRows) {\n",
       "    var geom = BrunelD3.geometry(parentNode || vis.node(), 0.05, 0.6, 0.45, 1, 0, 0, 0, 0),\n",
       "      elements = [];                                              // array of elements in this chart\n",
       "\n",
       "    // Define groups for the chart parts ///////////////////////////////////////////////////////////\n",
       "\n",
       "    var chart =  vis.append('g').attr('class', 'chart2')\n",
       "      .attr('transform','translate(' + geom.chart_left + ',' + geom.chart_top + ')');\n",
       "    var overlay = chart.append('g').attr('class', 'element').attr('class', 'overlay');\n",
       "    var zoom = d3.zoom().scaleExtent([1/3,3]);\n",
       "    var zoomNode = overlay.append('rect').attr('class', 'overlay')\n",
       "      .attr('x', geom.inner_left).attr('y', geom.inner_top)\n",
       "      .attr('width', geom.inner_rawWidth).attr('height', geom.inner_rawHeight)\n",
       "      .style('cursor', 'default')\n",
       "      .node();\n",
       "    zoomNode.__zoom = d3.zoomIdentity;\n",
       "    chart.append('rect').attr('class', 'background').attr('width', geom.chart_right-geom.chart_left).attr('height', geom.chart_bottom-geom.chart_top);\n",
       "    var interior = chart.append('g').attr('class', 'interior zoomNone')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')')\n",
       "      .attr('clip-path', 'url(#clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart2_inner)');\n",
       "    interior.append('rect').attr('class', 'inner').attr('width', geom.inner_width).attr('height', geom.inner_height);\n",
       "    var gridGroup = interior.append('g').attr('class', 'grid');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart2_inner').append('rect')\n",
       "      .attr('x', 0).attr('y', 0)\n",
       "      .attr('width', geom.inner_rawWidth+1).attr('height', geom.inner_rawHeight+1);\n",
       "    var tree, expandState = [], collapseState = {};    // collapse state maps node IDs to true/false\n",
       "    var scale_x = d3.scaleLinear(), scale_y = d3.scaleLinear();\n",
       "    var base_scales = [scale_x, scale_y];                           // untransformed original scales\n",
       "    zoom.on('zoom', function(t, time) {\n",
       "        t = t || d3.event.transform;\n",
       "        zoomNode.__zoom = t;\n",
       "        interior.attr('class', 'interior ' + BrunelD3.zoomLabel(t.k));;\n",
       "        build(time || -1);\n",
       "    });\n",
       "\n",
       "    // Define element #1 ///////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    elements[0] = function() {\n",
       "      var original, processed,                           // data sets passed in and then transformed\n",
       "        element, data,                                 // brunel element information and brunel data\n",
       "        selection, merged;                                      // d3 selection and merged selection\n",
       "      var elementGroup = interior.append('g').attr('class', 'element1'),\n",
       "        main = elementGroup.append('g').attr('class', 'main'),\n",
       "        labels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'labels').attr('aria-hidden', 'true'), elementGroup),\n",
       "        diagramLabels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'diagram labels').attr('aria-hidden', 'true'), elementGroup);\n",
       "\n",
       "      function makeData() {\n",
       "        original = datasets[0];\n",
       "        if (filterRows) original = original.retainRows(filterRows);\n",
       "        processed = pre(original, 0)\n",
       "          .sort('page_hits');\n",
       "        processed = post(processed, 0);\n",
       "        var f0 = processed.field('product_line'),\n",
       "          f1 = processed.field('page_hits'),\n",
       "          f2 = processed.field('#selection'),\n",
       "          f3 = processed.field('#row');\n",
       "        var keyFunc = function(d) { return f0.value(d) };\n",
       "        data = {\n",
       "          product_line: function(d) { return f0.value(d.row) },\n",
       "          page_hits:    function(d) { return f1.value(d.row) },\n",
       "          $selection:   function(d) { return f2.value(d.row) },\n",
       "          $row:         function(d) { return f3.value(d.row) },\n",
       "          product_line_f:function(d) { return f0.valueFormatted(d.row) },\n",
       "          page_hits_f:  function(d) { return f1.valueFormatted(d.row) },\n",
       "          $selection_f: function(d) { return f2.valueFormatted(d.row) },\n",
       "          $row_f:       function(d) { return f3.valueFormatted(d.row) },\n",
       "          _split:       function(d) { return f0.value(d.row)+ '|' + f2.value(d.row)+ '|' + f1.value(d.row) },\n",
       "          _key:         keyFunc,\n",
       "          _rows:        BrunelD3.makeRowsWithKeys(keyFunc, processed.rowCount())\n",
       "        };\n",
       "      }\n",
       "      // Aesthetic Functions\n",
       "      var scale_color = d3.scaleOrdinal()\n",
       "        .domain(['smartphones', 'computers', 'videogames', 'appliances', 'headphones', 'hometheater', 'cameras', 'carelectronics'])\n",
       "        .range([ '#347DAD', '#D43F58', '#F7D84A', '#31A461', '#A66A9C', '#FF954D', \n",
       "          '#A7978E', '#FFCA4D', '#F99EAF', '#B1C43B', '#7E64A2', '#FFB04D', '#CA5C7C', \n",
       "          '#DDBC8C', '#FFA28D', '#A5473D', '#8B6141', '#F57357', '#5C6B46']);\n",
       "      var color = function(d) { return scale_color(data.product_line(d.data)) };\n",
       "      var scale_opacity = d3.scaleOrdinal()\n",
       "        .domain(['✗', '✓'])\n",
       "        .range([0.2, 1.0]);\n",
       "      var opacity = function(d) { return scale_opacity(data.$selection(d.data)) };\n",
       "      var scale_size = d3.scaleLinear().domain([0, 58.000006])\n",
       "        .range([ 0.001, 1]);\n",
       "      var size = function(d) { return scale_size(data.page_hits(d.data)) };\n",
       "\n",
       "      // Build element from data ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "      function build(transitionMillis) {\n",
       "        element = elements[0];\n",
       "        // Define treemap (hierarchy) data structures\n",
       "        var first = (!tree), graph = BrunelData.diagram_Hierarchical.makeByNestingFields(processed, 'page_hits');\n",
       "        var targetNode = graph.root;\n",
       "        tree = d3.hierarchy(targetNode).sum(function(d) { return d.value });\n",
       "        BrunelD3.prune(tree, collapseState, true);\n",
       "        function nodeKey(d) { return d.data.key == null ? data._key(d.data.row) : d.data.key };\n",
       "        var treemap = d3.treemap().tile(d3.treemapResquarify)\n",
       "          .size([geom.inner_width, geom.inner_height])\n",
       "          .padding(function(d) { return d.depth < 3 ? 2*d.depth : 0} )\n",
       "          .paddingTop(function(d) { return d.depth ==1 ? 15 : (d.depth == 2) ? 12 : 0});\n",
       "        main.attr('class', 'diagram treemap hierarchy');\n",
       "        var labeling  = [{\n",
       "          index: 0, method: 'box', location: ['center', 'center'], inside: true, align: 'middle', pad: 3, dy: 0.3,\n",
       "          fit: true, granularity: 1,\n",
       "          content: function(d) {\n",
       "            return d.row == null ? null : data.product_line_f(d)\n",
       "          }\n",
       "        }];\n",
       "\n",
       "        // Define selection entry operations\n",
       "        function initialState(selection) {\n",
       "          selection\n",
       "            .attr('class', 'element polygon filled')\n",
       "            .filter(function(d) { return d.parent })\n",
       "            .attr('x', function(d) { return scale_x((d.parent.x0+d.parent.x1)/2) })\n",
       "            .attr('y', function(d) { return scale_y((d.parent.y0+d.parent.y1)/2) })\n",
       "            .attr('width', 0).attr('height', 0)\n",
       "        }\n",
       "\n",
       "        // Define selection update operations on merged data\n",
       "        function updateState(selection) {\n",
       "          selection\n",
       "            .attr('class', function(d) { return (d.collapsed ? 'collapsed ' : '') + (d.data.children ? 'element L' + d.depth : 'leaf element bar') })\n",
       "            .attr('x', function(d) { return scale_x(d.x0) })\n",
       "            .attr('y', function(d) { return scale_y(d.y0) })\n",
       "            .attr('width', function(d) { return scale_x(d.x1) - scale_x(d.x0) })\n",
       "            .attr('height', function(d) { return scale_y(d.y1) - scale_y(d.y0) })\n",
       "            .filter(BrunelD3.hasData)                     // following only performed for data items\n",
       "            .style('fill', color)\n",
       "            .style('fill-opacity', opacity)\n",
       "            .style('stroke-opacity', opacity);\n",
       "        }\n",
       "\n",
       "        // Define labeling for the selection\n",
       "        function label(selection, transitionMillis) {\n",
       "          BrunelD3.label(selection, labels, transitionMillis, geom, labeling);\n",
       "\n",
       "          var tooltipLabeling  = {\n",
       "            index: -1, method: 'box', location: ['center', 'top'], inside: true, align: 'middle', pad: 0, dy: 0.7,\n",
       "            fit: true, granularity: 0,\n",
       "            content: function(d) {\n",
       "              return d.row == null ? null : 'page views: '\n",
       "\t\t\t+ '<span class=\"field\">' + data.page_hits_f(d) + '</span>'\n",
       "            }\n",
       "          };\n",
       "          BrunelD3.addTooltip(selection, tooltipLabeling, geom);\n",
       "          diagramLabels.attr('class', 'axis diagram treemap hierarchy');\n",
       "          var treeLabeling = [{ index:0, method:'inner-left', fit:true, dy:0.83, align:'start', \n",
       "            content:  function(d) { return d.data.innerNodeName },\n",
       "            cssClass: function(d) { return 'axis label L' + d.depth + ' H' + d.height }, \n",
       "            where :   function(box) { return {'x': box.x + 2, 'y': box.y, 'box': box} }\n",
       "          }];\n",
       "          BrunelD3.label(selection.filter(function(d) {return d.data.key}), diagramLabels, transitionMillis, geom, treeLabeling);\n",
       "        }\n",
       "        // Create selections, set the initial state and transition updates\n",
       "        selection = main.selectAll('.element').data(treemap(tree).descendants(), nodeKey);\n",
       "        var added = selection.enter().append('rect');\n",
       "        merged = selection.merge(added);\n",
       "        initialState(added);\n",
       "        selection.filter(BrunelD3.hasData)\n",
       "          .classed('selected', BrunelD3.isSelected(data))\n",
       "          .filter(BrunelD3.isSelected(data)).raise();\n",
       "        updateState(BrunelD3.transition(merged, transitionMillis));\n",
       "        label(merged, transitionMillis);\n",
       "\n",
       "        BrunelD3.transition(selection.exit(), transitionMillis/3)\n",
       "          .style('opacity', 0.5).each( function() {\n",
       "            this.remove(); BrunelD3.removeLabels(this); \n",
       "        });\n",
       "        merged                                                     // attach handlers to the element\n",
       "          .on('dblclick.collapse', function(d) {if (d.data.children) {collapseState[d.data.key] = !collapseState[d.data.key]; charts[1].build(500)}  });\n",
       "      }\n",
       "\n",
       "      return {\n",
       "        data:           function() { return processed },\n",
       "        original:       function() { return original },\n",
       "        internal:       function() { return data },\n",
       "        selection:      function() { return merged },\n",
       "        makeData:       makeData,\n",
       "        build:          build,\n",
       "        chart:          function() { return charts[1] },\n",
       "        group:          function() { return elementGroup },\n",
       "        fields: {\n",
       "          key:          ['product_line'],\n",
       "          color:        ['product_line'],\n",
       "          size:         ['page_hits'],\n",
       "          opacity:      ['#selection']\n",
       "        }\n",
       "      };\n",
       "    }();\n",
       "\n",
       "    function build(time, noData) {\n",
       "      var first = elements[0].data() == null;\n",
       "      if (first) time = 0;                                           // no transition for first call\n",
       "      if ((first || time > -1) && !noData) {\n",
       "        elements[0].makeData();\n",
       "      }\n",
       "      elements[0].build(time);\n",
       "    }\n",
       "\n",
       "    // Expose the following components of the chart\n",
       "    return {\n",
       "      elements : elements,\n",
       "      interior : interior,\n",
       "      zoom: function(params, time) {\n",
       "          if (params) zoom.on('zoom').call(zoomNode, params, time);\n",
       "          return d3.zoomTransform(zoomNode);\n",
       "      },\n",
       "      build : build\n",
       "    };\n",
       "    }();\n",
       "\n",
       "  // Define chart #3 in the visualization //////////////////////////////////////////////////////////\n",
       "\n",
       "  charts[2] = function(parentNode, filterRows) {\n",
       "    var geom = BrunelD3.geometry(parentNode || vis.node(), 0.5, 0, 1, 0.5, 22, 49, 93, 0),\n",
       "      elements = [];                                              // array of elements in this chart\n",
       "\n",
       "    // Define groups for the chart parts ///////////////////////////////////////////////////////////\n",
       "\n",
       "    var chart =  vis.append('g').attr('class', 'chart3')\n",
       "      .attr('transform','translate(' + geom.chart_left + ',' + geom.chart_top + ')');\n",
       "    var overlay = chart.append('g').attr('class', 'element').attr('class', 'overlay');\n",
       "    var zoom = d3.zoom().scaleExtent([1/3,3]);\n",
       "    var zoomNode = overlay.append('rect').attr('class', 'overlay')\n",
       "      .attr('x', geom.inner_left).attr('y', geom.inner_top)\n",
       "      .attr('width', geom.inner_rawWidth).attr('height', geom.inner_rawHeight)\n",
       "      .style('cursor', 'move').call(zoom)\n",
       "      .node();\n",
       "    zoomNode.__zoom = d3.zoomIdentity;\n",
       "    chart.append('rect').attr('class', 'background').attr('width', geom.chart_right-geom.chart_left).attr('height', geom.chart_bottom-geom.chart_top);\n",
       "    var interior = chart.append('g').attr('class', 'interior zoomNone')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')')\n",
       "      .attr('clip-path', 'url(#clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart3_inner)');\n",
       "    interior.append('rect').attr('class', 'inner').attr('width', geom.inner_width).attr('height', geom.inner_height);\n",
       "    var gridGroup = interior.append('g').attr('class', 'grid');\n",
       "    var axes = chart.append('g').attr('class', 'axis')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart3_inner').append('rect')\n",
       "      .attr('x', 0).attr('y', 0)\n",
       "      .attr('width', geom.inner_rawWidth+1).attr('height', geom.inner_rawHeight+1);\n",
       "    chart.append('text').attr('class', 'title header').text('Total Time by Product Line').style('text-anchor', 'middle')\n",
       "      .attr('x','50%')\n",
       "      .attr('y',2).attr('dy','0.8em');\n",
       "\n",
       "    // Scales //////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    var scale_x = d3.scalePoint().padding(0.5)\n",
       "      .domain(['smartphones', 'computers', 'videogames', 'appliances', 'headphones', 'hometheater', 'cameras', 'carelectronics'])\n",
       "      .range([0, geom.inner_width]);\n",
       "    var scale_inner = d3.scaleLinear().domain([0,1])\n",
       "      .range([-0.5, 0.5]);\n",
       "    var scale_y = d3.scaleLinear().domain([0, 7000.0007])\n",
       "      .range([geom.inner_height, 0]);\n",
       "    var base_scales = [scale_x, scale_y];                           // untransformed original scales\n",
       "\n",
       "    // Axes ////////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    axes.append('g').attr('class', 'x axis')\n",
       "      .attr('transform','translate(0,' + geom.inner_rawHeight + ')')\n",
       "      .attr('clip-path', 'url(#clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart3_haxis)');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart3_haxis').append('polyline')\n",
       "      .attr('points', '-1,-1000, -1,-1 -5,5, -1000,5, -100,1000, 10000,1000 10000,-1000');\n",
       "    axes.select('g.axis.x').append('text').attr('class', 'title').text('product lines').style('text-anchor', 'middle')\n",
       "      .attr('x',geom.inner_rawWidth/2)\n",
       "      .attr('y', geom.inner_bottom - 2.0).attr('dy','-0.27em');\n",
       "    axes.append('g').attr('class', 'y axis')\n",
       "      .attr('clip-path', 'url(#clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart3_vaxis)');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart3_vaxis').append('polyline')\n",
       "      .attr('points', '-1000,-10000, 10000,-10000, 10000,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+5) + ', -1000,' + (geom.inner_rawHeight+5) );\n",
       "    axes.select('g.axis.y').append('text').attr('class', 'title').text('total time').style('text-anchor', 'middle')\n",
       "      .attr('x',-geom.inner_rawHeight/2)\n",
       "      .attr('y', 4-geom.inner_left).attr('dy', '0.7em').attr('transform', 'rotate(270)');\n",
       "\n",
       "    var axis_bottom = d3.axisBottom(scale_x).ticks(Math.min(10, Math.round(geom.inner_width / 124.5)));\n",
       "    var axis_left = d3.axisLeft(scale_y).ticks(Math.min(10, Math.round(geom.inner_width / 20)));\n",
       "\n",
       "    function buildAxes(time) {\n",
       "      axis_bottom.tickValues(BrunelD3.filterTicks(scale_x))\n",
       "      var axis_x = axes.select('g.axis.x');\n",
       "      BrunelD3.transition(axis_x, time).call(axis_bottom.scale(scale_x)).selectAll('.tick text')\n",
       "        .attr('transform', function() {\n",
       "          var v = this.getComputedTextLength() / Math.sqrt(2)/2;\n",
       "          return 'translate(-' + (v+6) + ',' + v + ') rotate(-45)'\n",
       "      });\n",
       "      var axis_y = axes.select('g.axis.y');\n",
       "      BrunelD3.transition(axis_y, time).call(axis_left.scale(scale_y));\n",
       "    }\n",
       "    zoom.on('zoom', function(t, time) {\n",
       "        t = t ||BrunelD3.restrictZoom(d3.event.transform, geom, this);\n",
       "        scale_y = t.rescaleY(base_scales[1]);\n",
       "        zoomNode.__zoom = t;\n",
       "        interior.attr('class', 'interior ' + BrunelD3.zoomLabel(t.k));;\n",
       "        build(time || -1);\n",
       "    });\n",
       "\n",
       "    // Define element #1 ///////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    elements[0] = function() {\n",
       "      var original, processed,                           // data sets passed in and then transformed\n",
       "        element, data,                                 // brunel element information and brunel data\n",
       "        selection, merged;                                      // d3 selection and merged selection\n",
       "      var elementGroup = interior.append('g').attr('class', 'element1'),\n",
       "        main = elementGroup.append('g').attr('class', 'main'),\n",
       "        labels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'labels').attr('aria-hidden', 'true'), elementGroup);\n",
       "\n",
       "      function makeData() {\n",
       "        original = datasets[0];\n",
       "        if (filterRows) original = original.retainRows(filterRows);\n",
       "        processed = pre(original, 0)\n",
       "          .sort('page_hits');\n",
       "        processed = post(processed, 0);\n",
       "        var f0 = processed.field('product_line'),\n",
       "          f1 = processed.field('total_time'),\n",
       "          f2 = processed.field('page_hits'),\n",
       "          f3 = processed.field('#row'),\n",
       "          f4 = processed.field('#selection');\n",
       "        var keyFunc = function(d) { return f0.value(d) };\n",
       "        data = {\n",
       "          product_line: function(d) { return f0.value(d.row) },\n",
       "          total_time:   function(d) { return f1.value(d.row) },\n",
       "          page_hits:    function(d) { return f2.value(d.row) },\n",
       "          $row:         function(d) { return f3.value(d.row) },\n",
       "          $selection:   function(d) { return f4.value(d.row) },\n",
       "          product_line_f:function(d) { return f0.valueFormatted(d.row) },\n",
       "          total_time_f: function(d) { return f1.valueFormatted(d.row) },\n",
       "          page_hits_f:  function(d) { return f2.valueFormatted(d.row) },\n",
       "          $row_f:       function(d) { return f3.valueFormatted(d.row) },\n",
       "          $selection_f: function(d) { return f4.valueFormatted(d.row) },\n",
       "          _split:       function(d) { return f0.value(d.row) },\n",
       "          _key:         keyFunc,\n",
       "          _rows:        BrunelD3.makeRowsWithKeys(keyFunc, processed.rowCount())\n",
       "        };\n",
       "      }\n",
       "      // Aesthetic Functions\n",
       "      var scale_color = d3.scaleOrdinal()\n",
       "        .domain(['smartphones', 'computers', 'videogames', 'appliances', 'headphones', 'hometheater', 'cameras', 'carelectronics'])\n",
       "        .range([ '#347DAD', '#D43F58', '#F7D84A', '#31A461', '#A66A9C', '#FF954D', \n",
       "          '#A7978E', '#FFCA4D', '#F99EAF', '#B1C43B', '#7E64A2', '#FFB04D', '#CA5C7C', \n",
       "          '#DDBC8C', '#FFA28D', '#A5473D', '#8B6141', '#F57357', '#5C6B46']);\n",
       "      var color = function(d) { return scale_color(data.product_line(d)) };\n",
       "\n",
       "      // Build element from data ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "      function build(transitionMillis) {\n",
       "        element = elements[0];\n",
       "        var w = 0.9 * Math.abs(scale_x(scale_x.domain()[1]) - scale_x(scale_x.domain()[0]) );\n",
       "        var x = function(d) { return scale_x(data.product_line(d))};\n",
       "        var h = geom.default_point_size;\n",
       "        var y1 = scale_y.range()[0];\n",
       "        var y2 = function(d) { return scale_y(data.total_time(d))};\n",
       "\n",
       "        // Define selection entry operations\n",
       "        function initialState(selection) {\n",
       "          selection\n",
       "            .attr('class', 'element bar filled')\n",
       "        }\n",
       "\n",
       "        // Define selection update operations on merged data\n",
       "        function updateState(selection) {\n",
       "          selection\n",
       "            .each(function(d) {\n",
       "              var width = w, left = x(d) - width/2, \n",
       "              c = y1, d = y2(d), top = Math.min(c,d), height = Math.max(1e-6, Math.abs(c-d));\n",
       "              this.r = {x:left, y:top, w:width, h:height};\n",
       "            })\n",
       "            .attr('x', function(d) { return this.r.x })\n",
       "            .attr('y', function(d) { return this.r.y })\n",
       "            .attr('width', function(d) { return this.r.w })\n",
       "            .attr('height', function(d) { return this.r.h })\n",
       "            .filter(BrunelD3.hasData)                     // following only performed for data items\n",
       "            .style('fill', color);\n",
       "        }\n",
       "\n",
       "        // Define labeling for the selection\n",
       "        function label(selection, transitionMillis) {\n",
       "\n",
       "          var tooltipLabeling  = {\n",
       "            index: -1, method: 'box', location: ['center', 'top'], inside: true, align: 'middle', pad: 0, dy: 0.7,\n",
       "            fit: true, granularity: 0,\n",
       "            content: function(d) {\n",
       "              return d.row == null ? null : '<span class=\"title\">product_line: </span>'\n",
       "\t\t\t+ '<span class=\"field\">' + data.product_line_f(d) + '</span>'\n",
       "\t\t\t+ '<br/>'\n",
       "\t\t\t+ '<span class=\"title\">total_time: </span>'\n",
       "\t\t\t+ '<span class=\"field\">' + data.total_time_f(d) + '</span>'\n",
       "\t\t\t+ '<br/>'\n",
       "\t\t\t+ '<span class=\"title\">page_hits: </span>'\n",
       "\t\t\t+ '<span class=\"field\">' + data.page_hits_f(d) + '</span>'\n",
       "            }\n",
       "          };\n",
       "          BrunelD3.addTooltip(selection, tooltipLabeling, geom);\n",
       "        }\n",
       "        // Create selections, set the initial state and transition updates\n",
       "        selection = main.selectAll('.element').data(data._rows, function(d) { return d.key });\n",
       "        var added = selection.enter().append('rect');\n",
       "        merged = selection.merge(added);\n",
       "        initialState(added);\n",
       "        selection.filter(BrunelD3.hasData)\n",
       "          .classed('selected', BrunelD3.isSelected(data))\n",
       "          .filter(BrunelD3.isSelected(data)).raise();\n",
       "        updateState(BrunelD3.transition(merged, transitionMillis));\n",
       "        label(merged, transitionMillis);\n",
       "\n",
       "        BrunelD3.transition(selection.exit(), transitionMillis/3)\n",
       "          .style('opacity', 0.5).each( function() {\n",
       "            this.remove(); BrunelD3.removeLabels(this); \n",
       "        });\n",
       "        chart.select('rect.overlay')                               // attach handlers to the overlay\n",
       "          .on('click.interact', function(d) {\n",
       "            var c = BrunelD3.closest(merged, 'xy', geom.inner_radius/4 );\n",
       "            BrunelD3.select(null, this, element, updateAll);\n",
       "          });\n",
       "        merged                                                     // attach handlers to the element\n",
       "          .on('click.interact', function(d) {BrunelD3.select(d, this, element, updateAll) });\n",
       "      }\n",
       "\n",
       "      return {\n",
       "        data:           function() { return processed },\n",
       "        original:       function() { return original },\n",
       "        internal:       function() { return data },\n",
       "        selection:      function() { return merged },\n",
       "        makeData:       makeData,\n",
       "        build:          build,\n",
       "        chart:          function() { return charts[2] },\n",
       "        group:          function() { return elementGroup },\n",
       "        fields: {\n",
       "          x:            ['product_line'],\n",
       "          y:            ['total_time'],\n",
       "          key:          ['product_line'],\n",
       "          color:        ['product_line']\n",
       "        }\n",
       "      };\n",
       "    }();\n",
       "\n",
       "    function build(time, noData) {\n",
       "      var first = elements[0].data() == null;\n",
       "      if (first) time = 0;                                           // no transition for first call\n",
       "      buildAxes(time);\n",
       "      if ((first || time > -1) && !noData) {\n",
       "        elements[0].makeData();\n",
       "      }\n",
       "      elements[0].build(time);\n",
       "    }\n",
       "\n",
       "    // Expose the following components of the chart\n",
       "    return {\n",
       "      elements : elements,\n",
       "      interior : interior,\n",
       "      scales: {x:scale_x, y:scale_y},\n",
       "      zoom: function(params, time) {\n",
       "          if (params) zoom.on('zoom').call(zoomNode, params, time);\n",
       "          return d3.zoomTransform(zoomNode);\n",
       "      },\n",
       "      build : build\n",
       "    };\n",
       "    }();\n",
       "\n",
       "  // Define chart #4 in the visualization //////////////////////////////////////////////////////////\n",
       "\n",
       "  charts[3] = function(parentNode, filterRows) {\n",
       "    var geom = BrunelD3.geometry(parentNode || vis.node(), 0.55, 0.6, 0.95, 1, 0, 0, 0, 0),\n",
       "      elements = [];                                              // array of elements in this chart\n",
       "\n",
       "    // Define groups for the chart parts ///////////////////////////////////////////////////////////\n",
       "\n",
       "    var chart =  vis.append('g').attr('class', 'chart4')\n",
       "      .attr('transform','translate(' + geom.chart_left + ',' + geom.chart_top + ')');\n",
       "    var overlay = chart.append('g').attr('class', 'element').attr('class', 'overlay');\n",
       "    var zoom = d3.zoom().scaleExtent([1/3,3]);\n",
       "    var zoomNode = overlay.append('rect').attr('class', 'overlay')\n",
       "      .attr('x', geom.inner_left).attr('y', geom.inner_top)\n",
       "      .attr('width', geom.inner_rawWidth).attr('height', geom.inner_rawHeight)\n",
       "      .style('cursor', 'default')\n",
       "      .node();\n",
       "    zoomNode.__zoom = d3.zoomIdentity;\n",
       "    chart.append('rect').attr('class', 'background').attr('width', geom.chart_right-geom.chart_left).attr('height', geom.chart_bottom-geom.chart_top);\n",
       "    var interior = chart.append('g').attr('class', 'interior zoomNone')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')')\n",
       "      .attr('clip-path', 'url(#clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart4_inner)');\n",
       "    interior.append('rect').attr('class', 'inner').attr('width', geom.inner_width).attr('height', geom.inner_height);\n",
       "    var gridGroup = interior.append('g').attr('class', 'grid');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidee9cc519-cb99-41be-93da-c926b0428acd_chart4_inner').append('rect')\n",
       "      .attr('x', 0).attr('y', 0)\n",
       "      .attr('width', geom.inner_rawWidth+1).attr('height', geom.inner_rawHeight+1);\n",
       "    var tree, expandState = [], collapseState = {};    // collapse state maps node IDs to true/false\n",
       "    var scale_x = d3.scaleLinear(), scale_y = d3.scaleLinear();\n",
       "    var base_scales = [scale_x, scale_y];                           // untransformed original scales\n",
       "    zoom.on('zoom', function(t, time) {\n",
       "        t = t || d3.event.transform;\n",
       "        zoomNode.__zoom = t;\n",
       "        interior.attr('class', 'interior ' + BrunelD3.zoomLabel(t.k));;\n",
       "        build(time || -1);\n",
       "    });\n",
       "\n",
       "    // Define element #1 ///////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    elements[0] = function() {\n",
       "      var original, processed,                           // data sets passed in and then transformed\n",
       "        element, data,                                 // brunel element information and brunel data\n",
       "        selection, merged;                                      // d3 selection and merged selection\n",
       "      var elementGroup = interior.append('g').attr('class', 'element1'),\n",
       "        main = elementGroup.append('g').attr('class', 'main'),\n",
       "        labels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'labels').attr('aria-hidden', 'true'), elementGroup),\n",
       "        diagramLabels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'diagram labels').attr('aria-hidden', 'true'), elementGroup);\n",
       "\n",
       "      function makeData() {\n",
       "        original = datasets[0];\n",
       "        if (filterRows) original = original.retainRows(filterRows);\n",
       "        processed = pre(original, 0)\n",
       "          .sort('page_hits');\n",
       "        processed = post(processed, 0);\n",
       "        var f0 = processed.field('product_line'),\n",
       "          f1 = processed.field('total_time'),\n",
       "          f2 = processed.field('#selection'),\n",
       "          f3 = processed.field('page_hits'),\n",
       "          f4 = processed.field('#row');\n",
       "        var keyFunc = function(d) { return f0.value(d) };\n",
       "        data = {\n",
       "          product_line: function(d) { return f0.value(d.row) },\n",
       "          total_time:   function(d) { return f1.value(d.row) },\n",
       "          $selection:   function(d) { return f2.value(d.row) },\n",
       "          page_hits:    function(d) { return f3.value(d.row) },\n",
       "          $row:         function(d) { return f4.value(d.row) },\n",
       "          product_line_f:function(d) { return f0.valueFormatted(d.row) },\n",
       "          total_time_f: function(d) { return f1.valueFormatted(d.row) },\n",
       "          $selection_f: function(d) { return f2.valueFormatted(d.row) },\n",
       "          page_hits_f:  function(d) { return f3.valueFormatted(d.row) },\n",
       "          $row_f:       function(d) { return f4.valueFormatted(d.row) },\n",
       "          _split:       function(d) { return f0.value(d.row)+ '|' + f2.value(d.row)+ '|' + f1.value(d.row) },\n",
       "          _key:         keyFunc,\n",
       "          _rows:        BrunelD3.makeRowsWithKeys(keyFunc, processed.rowCount())\n",
       "        };\n",
       "      }\n",
       "      // Aesthetic Functions\n",
       "      var scale_color = d3.scaleOrdinal()\n",
       "        .domain(['smartphones', 'computers', 'videogames', 'appliances', 'headphones', 'hometheater', 'cameras', 'carelectronics'])\n",
       "        .range([ '#347DAD', '#D43F58', '#F7D84A', '#31A461', '#A66A9C', '#FF954D', \n",
       "          '#A7978E', '#FFCA4D', '#F99EAF', '#B1C43B', '#7E64A2', '#FFB04D', '#CA5C7C', \n",
       "          '#DDBC8C', '#FFA28D', '#A5473D', '#8B6141', '#F57357', '#5C6B46']);\n",
       "      var color = function(d) { return scale_color(data.product_line(d.data)) };\n",
       "      var scale_opacity = d3.scaleOrdinal()\n",
       "        .domain(['✗', '✓'])\n",
       "        .range([0.2, 1.0]);\n",
       "      var opacity = function(d) { return scale_opacity(data.$selection(d.data)) };\n",
       "      var scale_size = d3.scaleLinear().domain([0, 6360.0006])\n",
       "        .range([ 0.001, 1]);\n",
       "      var size = function(d) { return scale_size(data.total_time(d.data)) };\n",
       "\n",
       "      // Build element from data ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "      function build(transitionMillis) {\n",
       "        element = elements[0];\n",
       "        // Define treemap (hierarchy) data structures\n",
       "        var first = (!tree), graph = BrunelData.diagram_Hierarchical.makeByNestingFields(processed, 'total_time');\n",
       "        var targetNode = graph.root;\n",
       "        tree = d3.hierarchy(targetNode).sum(function(d) { return d.value });\n",
       "        BrunelD3.prune(tree, collapseState, true);\n",
       "        function nodeKey(d) { return d.data.key == null ? data._key(d.data.row) : d.data.key };\n",
       "        var treemap = d3.treemap().tile(d3.treemapResquarify)\n",
       "          .size([geom.inner_width, geom.inner_height])\n",
       "          .padding(function(d) { return d.depth < 3 ? 2*d.depth : 0} )\n",
       "          .paddingTop(function(d) { return d.depth ==1 ? 15 : (d.depth == 2) ? 12 : 0});\n",
       "        main.attr('class', 'diagram treemap hierarchy');\n",
       "        var labeling  = [{\n",
       "          index: 0, method: 'box', location: ['center', 'center'], inside: true, align: 'middle', pad: 3, dy: 0.3,\n",
       "          fit: true, granularity: 1,\n",
       "          content: function(d) {\n",
       "            return d.row == null ? null : data.product_line_f(d)\n",
       "          }\n",
       "        }];\n",
       "\n",
       "        // Define selection entry operations\n",
       "        function initialState(selection) {\n",
       "          selection\n",
       "            .attr('class', 'element polygon filled')\n",
       "            .filter(function(d) { return d.parent })\n",
       "            .attr('x', function(d) { return scale_x((d.parent.x0+d.parent.x1)/2) })\n",
       "            .attr('y', function(d) { return scale_y((d.parent.y0+d.parent.y1)/2) })\n",
       "            .attr('width', 0).attr('height', 0)\n",
       "        }\n",
       "\n",
       "        // Define selection update operations on merged data\n",
       "        function updateState(selection) {\n",
       "          selection\n",
       "            .attr('class', function(d) { return (d.collapsed ? 'collapsed ' : '') + (d.data.children ? 'element L' + d.depth : 'leaf element bar') })\n",
       "            .attr('x', function(d) { return scale_x(d.x0) })\n",
       "            .attr('y', function(d) { return scale_y(d.y0) })\n",
       "            .attr('width', function(d) { return scale_x(d.x1) - scale_x(d.x0) })\n",
       "            .attr('height', function(d) { return scale_y(d.y1) - scale_y(d.y0) })\n",
       "            .filter(BrunelD3.hasData)                     // following only performed for data items\n",
       "            .style('fill', color)\n",
       "            .style('fill-opacity', opacity)\n",
       "            .style('stroke-opacity', opacity);\n",
       "        }\n",
       "\n",
       "        // Define labeling for the selection\n",
       "        function label(selection, transitionMillis) {\n",
       "          BrunelD3.label(selection, labels, transitionMillis, geom, labeling);\n",
       "\n",
       "          var tooltipLabeling  = {\n",
       "            index: -1, method: 'box', location: ['center', 'top'], inside: true, align: 'middle', pad: 0, dy: 0.7,\n",
       "            fit: true, granularity: 0,\n",
       "            content: function(d) {\n",
       "              return d.row == null ? null : 'time on page (sec): '\n",
       "\t\t\t+ '<span class=\"field\">' + data.total_time_f(d) + '</span>'\n",
       "            }\n",
       "          };\n",
       "          BrunelD3.addTooltip(selection, tooltipLabeling, geom);\n",
       "          diagramLabels.attr('class', 'axis diagram treemap hierarchy');\n",
       "          var treeLabeling = [{ index:0, method:'inner-left', fit:true, dy:0.83, align:'start', \n",
       "            content:  function(d) { return d.data.innerNodeName },\n",
       "            cssClass: function(d) { return 'axis label L' + d.depth + ' H' + d.height }, \n",
       "            where :   function(box) { return {'x': box.x + 2, 'y': box.y, 'box': box} }\n",
       "          }];\n",
       "          BrunelD3.label(selection.filter(function(d) {return d.data.key}), diagramLabels, transitionMillis, geom, treeLabeling);\n",
       "        }\n",
       "        // Create selections, set the initial state and transition updates\n",
       "        selection = main.selectAll('.element').data(treemap(tree).descendants(), nodeKey);\n",
       "        var added = selection.enter().append('rect');\n",
       "        merged = selection.merge(added);\n",
       "        initialState(added);\n",
       "        selection.filter(BrunelD3.hasData)\n",
       "          .classed('selected', BrunelD3.isSelected(data))\n",
       "          .filter(BrunelD3.isSelected(data)).raise();\n",
       "        updateState(BrunelD3.transition(merged, transitionMillis));\n",
       "        label(merged, transitionMillis);\n",
       "\n",
       "        BrunelD3.transition(selection.exit(), transitionMillis/3)\n",
       "          .style('opacity', 0.5).each( function() {\n",
       "            this.remove(); BrunelD3.removeLabels(this); \n",
       "        });\n",
       "        merged                                                     // attach handlers to the element\n",
       "          .on('dblclick.collapse', function(d) {if (d.data.children) {collapseState[d.data.key] = !collapseState[d.data.key]; charts[3].build(500)}  });\n",
       "      }\n",
       "\n",
       "      return {\n",
       "        data:           function() { return processed },\n",
       "        original:       function() { return original },\n",
       "        internal:       function() { return data },\n",
       "        selection:      function() { return merged },\n",
       "        makeData:       makeData,\n",
       "        build:          build,\n",
       "        chart:          function() { return charts[3] },\n",
       "        group:          function() { return elementGroup },\n",
       "        fields: {\n",
       "          key:          ['product_line'],\n",
       "          color:        ['product_line'],\n",
       "          size:         ['total_time'],\n",
       "          opacity:      ['#selection']\n",
       "        }\n",
       "      };\n",
       "    }();\n",
       "\n",
       "    function build(time, noData) {\n",
       "      var first = elements[0].data() == null;\n",
       "      if (first) time = 0;                                           // no transition for first call\n",
       "      if ((first || time > -1) && !noData) {\n",
       "        elements[0].makeData();\n",
       "      }\n",
       "      elements[0].build(time);\n",
       "    }\n",
       "\n",
       "    // Expose the following components of the chart\n",
       "    return {\n",
       "      elements : elements,\n",
       "      interior : interior,\n",
       "      zoom: function(params, time) {\n",
       "          if (params) zoom.on('zoom').call(zoomNode, params, time);\n",
       "          return d3.zoomTransform(zoomNode);\n",
       "      },\n",
       "      build : build\n",
       "    };\n",
       "    }();\n",
       "\n",
       "  function setData(rowData, i) { datasets[i||0] = BrunelD3.makeData(rowData) }\n",
       "  function updateAll(time) { charts.forEach(function(x) {x.build(time || 0)}) }\n",
       "  function buildAll() {\n",
       "    for (var i=0;i<arguments.length;i++) setData(arguments[i], i);\n",
       "    updateAll(transitionTime);\n",
       "  }\n",
       "\n",
       "  return {\n",
       "    dataPreProcess:     function(f) { if (f) pre = f; return pre },\n",
       "    dataPostProcess:    function(f) { if (f) post = f; return post },\n",
       "    data:               function(d,i) { if (d) setData(d,i); return datasets[i||0] },\n",
       "    visId:              visId,\n",
       "    build:              buildAll,\n",
       "    rebuild:            updateAll,\n",
       "    charts:             charts\n",
       "  }\n",
       "}\n",
       "\n",
       "// Data Tables /////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "var table1 = {\n",
       "   summarized: false,\n",
       "   names: ['product_line', 'page_hits', 'total_time'], \n",
       "   options: ['string', 'numeric', 'numeric'], \n",
       "   rows: [['smartphones', 58, 6360], ['computers', 16, 3720], ['videogames', 7, 4800],\n",
       "  ['appliances', 5, 2220], ['hometheater', 4, 840], ['headphones', 4, 960],\n",
       "  ['carelectronics', 2, 420], ['cameras', 2, 360]]\n",
       "};\n",
       "\n",
       "// Call Code to Build the system ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "var v = new BrunelVis('visidee9cc519-cb99-41be-93da-c926b0428acd');\n",
       "v .build(table1);\n",
       "\n",
       "            \"\"\n",
       "        });\n",
       "        });\n",
       "        </script>"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "%%brunel data('productlineMetrics') \n",
    "bar at(0,0,50,50) title(\"Page Views by Product Line\")\n",
    "  x(product_line) y(page_hits)\n",
    "  tooltip(#all) color(product_line) legends(none)\n",
    "  axes(x:'product lines',y:'page views') sort(page_hits) interaction(select)|\n",
    "treemap at(60,5,100,45)\n",
    "  sort(page_hits) size(page_hits) color(product_line) label(product_line) legends(none)\n",
    "  tooltip(\"page views: \",page_hits) opacity(#selection) | \n",
    "bar at(0,50,50,100) title(\"Total Time by Product Line\")\n",
    "  x(product_line) y(total_time)\n",
    "  tooltip(#all) color(product_line) legends(none)\n",
    "  axes(x:'product lines',y:'total time') sort(page_hits) interaction(select)|\n",
    "treemap at(60,55,100,95)\n",
    "  sort(page_hits) size(total_time) color(product_line) label(product_line) legends(none)\n",
    "  tooltip(\"time on page (sec): \",total_time) opacity(#selection)\n",
    ":: width=1000, height=600"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Aggregated web metrics for smart phones"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "collapsed": false,
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+------------+-------+---------+----------+\n",
      "|product_line|product|page_hits|total_time|\n",
      "+------------+-------+---------+----------+\n",
      "| smartphones|A-phone|       21|      1920|\n",
      "| smartphones|S-phone|       12|      1380|\n",
      "| smartphones|M-phone|        5|       540|\n",
      "| smartphones|L-phone|        3|       180|\n",
      "| smartphones|H-phone|        2|       120|\n",
      "| smartphones|X-phone|        1|       720|\n",
      "+------------+-------+---------+----------+\n",
      "\n"
     ]
    }
   ],
   "source": [
    "val productMetrics = webMetricsDF.\n",
    "  select(\"product_line\",\"product\",\"page_hits\",\"total_time\").\n",
    "  filter($\"action\" === \"details\").filter($\"product_line\" === \"smartphones\").\n",
    "  groupBy(\"product_line\",\"product\").agg(sum(\"page_hits\"), sum(\"total_time\")).\n",
    "  withColumnRenamed(\"sum(page_hits)\",\"page_hits\").\n",
    "  withColumnRenamed(\"sum(total_time)\",\"total_time\")\n",
    "productMetrics.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "         <link rel=\"stylesheet\" type=\"text/css\" href=\"https://brunelvis.org/js/brunel.2.3.css\" charset=\"utf-8\">\n",
       "         <link rel=\"stylesheet\" type=\"text/css\" href=\"https://brunelvis.org/js/sumoselect.css\" charset=\"utf-8\">\n",
       "         <style>  </style>\n",
       "         <div id=\"controlsId2e30bd78-def0-44ca-a735-6423693e3866\" class=\"brunel\"/>\n",
       "<svg id=\"visidac89e56a-7c13-4871-bd78-ce10c7429f4a\" width=\"1000\" height=\"600\"></svg>\n",
       "\n",
       "<script>\n",
       "require.config({\n",
       "            waitSeconds: 60,\n",
       "            paths: {\n",
       "                'd3': '//cdnjs.cloudflare.com/ajax/libs/d3/4.2.1/d3.min',\n",
       "                'topojson' : '//cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min',\n",
       "                'brunel' : 'https://brunelvis.org/js/brunel.2.3.min',\n",
       "                'brunelControls' : 'https://brunelvis.org/js/brunel.controls.2.3.min'\n",
       "            },\n",
       "\n",
       "            shim: {\n",
       "               'brunel' : {\n",
       "                    exports: 'BrunelD3',\n",
       "                    deps: ['d3', 'topojson'],\n",
       "                    init: function() {\n",
       "                       return {\n",
       "                         BrunelD3 : BrunelD3,\n",
       "                         BrunelData : BrunelData\n",
       "                      }\n",
       "                    }\n",
       "                },\n",
       "               'brunelControls' : {\n",
       "                    exports: 'BrunelEventHandlers',\n",
       "                    init: function() {\n",
       "                       return {\n",
       "                         BrunelEventHandlers: BrunelEventHandlers,\n",
       "                         BrunelJQueryControlFactory: BrunelJQueryControlFactory\n",
       "                      }\n",
       "                    }\n",
       "                }\n",
       "\n",
       "            }\n",
       "\n",
       "        });\n",
       "\n",
       "        require([\"d3\"], function(d3) {\n",
       "        require([\"brunel\", \"brunelControls\"], function(brunel, brunelControls) {\n",
       "\n",
       "            function  BrunelVis(visId) {\n",
       "  \"use strict\";                                                                       // strict mode\n",
       "  var datasets = [],                                      // array of datasets for the original data\n",
       "      pre = function(d, i) { return d },                         // default pre-process does nothing\n",
       "      post = function(d, i) { return d },                       // default post-process does nothing\n",
       "      transitionTime = 200,                                        // transition time for animations\n",
       "      charts = [],                                                       // the charts in the system\n",
       "      vis = d3.select('#' + visId).attr('class', 'brunel');                     // the SVG container\n",
       "\n",
       "  BrunelD3.addDefinitions(vis);                                   // ensure standard symbols present\n",
       "\n",
       "  // Define chart #1 in the visualization //////////////////////////////////////////////////////////\n",
       "\n",
       "  charts[0] = function(parentNode, filterRows) {\n",
       "    var geom = BrunelD3.geometry(parentNode || vis.node(), 0, 0, 0.5, 0.5, 22, 43, 37, 0),\n",
       "      elements = [];                                              // array of elements in this chart\n",
       "\n",
       "    // Define groups for the chart parts ///////////////////////////////////////////////////////////\n",
       "\n",
       "    var chart =  vis.append('g').attr('class', 'chart1')\n",
       "      .attr('transform','translate(' + geom.chart_left + ',' + geom.chart_top + ')');\n",
       "    var overlay = chart.append('g').attr('class', 'element').attr('class', 'overlay');\n",
       "    var zoom = d3.zoom().scaleExtent([1/3,3]);\n",
       "    var zoomNode = overlay.append('rect').attr('class', 'overlay')\n",
       "      .attr('x', geom.inner_left).attr('y', geom.inner_top)\n",
       "      .attr('width', geom.inner_rawWidth).attr('height', geom.inner_rawHeight)\n",
       "      .style('cursor', 'move').call(zoom)\n",
       "      .node();\n",
       "    zoomNode.__zoom = d3.zoomIdentity;\n",
       "    chart.append('rect').attr('class', 'background').attr('width', geom.chart_right-geom.chart_left).attr('height', geom.chart_bottom-geom.chart_top);\n",
       "    var interior = chart.append('g').attr('class', 'interior zoomNone')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')')\n",
       "      .attr('clip-path', 'url(#clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart1_inner)');\n",
       "    interior.append('rect').attr('class', 'inner').attr('width', geom.inner_width).attr('height', geom.inner_height);\n",
       "    var gridGroup = interior.append('g').attr('class', 'grid');\n",
       "    var axes = chart.append('g').attr('class', 'axis')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart1_inner').append('rect')\n",
       "      .attr('x', 0).attr('y', 0)\n",
       "      .attr('width', geom.inner_rawWidth+1).attr('height', geom.inner_rawHeight+1);\n",
       "    chart.append('text').attr('class', 'title header').text('Page Views by Smart Phone').style('text-anchor', 'middle')\n",
       "      .attr('x','50%')\n",
       "      .attr('y',2).attr('dy','0.8em');\n",
       "\n",
       "    // Scales //////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    var scale_x = d3.scalePoint().padding(0.5)\n",
       "      .domain(['A-phone', 'S-phone', 'M-phone', 'L-phone', 'H-phone', 'X-phone'])\n",
       "      .range([0, geom.inner_width]);\n",
       "    var scale_inner = d3.scaleLinear().domain([0,1])\n",
       "      .range([-0.5, 0.5]);\n",
       "    var scale_y = d3.scaleLinear().domain([0, 22.000002])\n",
       "      .range([geom.inner_height, 0]);\n",
       "    var base_scales = [scale_x, scale_y];                           // untransformed original scales\n",
       "\n",
       "    // Axes ////////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    axes.append('g').attr('class', 'x axis')\n",
       "      .attr('transform','translate(0,' + geom.inner_rawHeight + ')')\n",
       "      .attr('clip-path', 'url(#clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart1_haxis)');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart1_haxis').append('polyline')\n",
       "      .attr('points', '-1,-1000, -1,-1 -5,5, -1000,5, -100,1000, 10000,1000 10000,-1000');\n",
       "    axes.select('g.axis.x').append('text').attr('class', 'title').text('smart phones').style('text-anchor', 'middle')\n",
       "      .attr('x',geom.inner_rawWidth/2)\n",
       "      .attr('y', geom.inner_bottom - 2.0).attr('dy','-0.27em');\n",
       "    axes.append('g').attr('class', 'y axis')\n",
       "      .attr('clip-path', 'url(#clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart1_vaxis)');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart1_vaxis').append('polyline')\n",
       "      .attr('points', '-1000,-10000, 10000,-10000, 10000,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+5) + ', -1000,' + (geom.inner_rawHeight+5) );\n",
       "    axes.select('g.axis.y').append('text').attr('class', 'title').text('page views').style('text-anchor', 'middle')\n",
       "      .attr('x',-geom.inner_rawHeight/2)\n",
       "      .attr('y', 4-geom.inner_left).attr('dy', '0.7em').attr('transform', 'rotate(270)');\n",
       "\n",
       "    var axis_bottom = d3.axisBottom(scale_x).ticks(Math.min(10, Math.round(geom.inner_width / 66.0)));\n",
       "    var axis_left = d3.axisLeft(scale_y).ticks(Math.min(10, Math.round(geom.inner_width / 20)));\n",
       "\n",
       "    function buildAxes(time) {\n",
       "      axis_bottom.tickValues(BrunelD3.filterTicks(scale_x))\n",
       "      var axis_x = axes.select('g.axis.x');\n",
       "      BrunelD3.transition(axis_x, time).call(axis_bottom.scale(scale_x));\n",
       "      var axis_y = axes.select('g.axis.y');\n",
       "      BrunelD3.transition(axis_y, time).call(axis_left.scale(scale_y));\n",
       "    }\n",
       "    zoom.on('zoom', function(t, time) {\n",
       "        t = t ||BrunelD3.restrictZoom(d3.event.transform, geom, this);\n",
       "        scale_y = t.rescaleY(base_scales[1]);\n",
       "        zoomNode.__zoom = t;\n",
       "        interior.attr('class', 'interior ' + BrunelD3.zoomLabel(t.k));;\n",
       "        build(time || -1);\n",
       "    });\n",
       "\n",
       "    // Define element #1 ///////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    elements[0] = function() {\n",
       "      var original, processed,                           // data sets passed in and then transformed\n",
       "        element, data,                                 // brunel element information and brunel data\n",
       "        selection, merged;                                      // d3 selection and merged selection\n",
       "      var elementGroup = interior.append('g').attr('class', 'element1'),\n",
       "        main = elementGroup.append('g').attr('class', 'main'),\n",
       "        labels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'labels').attr('aria-hidden', 'true'), elementGroup);\n",
       "\n",
       "      function makeData() {\n",
       "        original = datasets[0];\n",
       "        if (filterRows) original = original.retainRows(filterRows);\n",
       "        processed = pre(original, 0)\n",
       "          .sort('page_hits');\n",
       "        processed = post(processed, 0);\n",
       "        var f0 = processed.field('product'),\n",
       "          f1 = processed.field('page_hits'),\n",
       "          f2 = processed.field('#row'),\n",
       "          f3 = processed.field('#selection');\n",
       "        var keyFunc = function(d) { return f0.value(d) };\n",
       "        data = {\n",
       "          product:      function(d) { return f0.value(d.row) },\n",
       "          page_hits:    function(d) { return f1.value(d.row) },\n",
       "          $row:         function(d) { return f2.value(d.row) },\n",
       "          $selection:   function(d) { return f3.value(d.row) },\n",
       "          product_f:    function(d) { return f0.valueFormatted(d.row) },\n",
       "          page_hits_f:  function(d) { return f1.valueFormatted(d.row) },\n",
       "          $row_f:       function(d) { return f2.valueFormatted(d.row) },\n",
       "          $selection_f: function(d) { return f3.valueFormatted(d.row) },\n",
       "          _split:       function(d) { return f0.value(d.row) },\n",
       "          _key:         keyFunc,\n",
       "          _rows:        BrunelD3.makeRowsWithKeys(keyFunc, processed.rowCount())\n",
       "        };\n",
       "      }\n",
       "      // Aesthetic Functions\n",
       "      var scale_color = d3.scaleOrdinal()\n",
       "        .domain(['A-phone', 'S-phone', 'M-phone', 'L-phone', 'H-phone', 'X-phone'])\n",
       "        .range([ '#347DAD', '#D43F58', '#F7D84A', '#31A461', '#A66A9C', '#FF954D', \n",
       "          '#A7978E', '#FFCA4D', '#F99EAF', '#B1C43B', '#7E64A2', '#FFB04D', '#CA5C7C', \n",
       "          '#DDBC8C', '#FFA28D', '#A5473D', '#8B6141', '#F57357', '#5C6B46']);\n",
       "      var color = function(d) { return scale_color(data.product(d)) };\n",
       "\n",
       "      // Build element from data ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "      function build(transitionMillis) {\n",
       "        element = elements[0];\n",
       "        var w = 0.9 * Math.abs(scale_x(scale_x.domain()[1]) - scale_x(scale_x.domain()[0]) );\n",
       "        var x = function(d) { return scale_x(data.product(d))};\n",
       "        var h = Math.abs( scale_y(scale_y.domain()[0] + 1.0) - scale_y.range()[0] );\n",
       "        var y1 = scale_y.range()[0];\n",
       "        var y2 = function(d) { return scale_y(data.page_hits(d))};\n",
       "\n",
       "        // Define selection entry operations\n",
       "        function initialState(selection) {\n",
       "          selection\n",
       "            .attr('class', 'element bar filled')\n",
       "        }\n",
       "\n",
       "        // Define selection update operations on merged data\n",
       "        function updateState(selection) {\n",
       "          selection\n",
       "            .each(function(d) {\n",
       "              var width = w, left = x(d) - width/2, \n",
       "              c = y1, d = y2(d), top = Math.min(c,d), height = Math.max(1e-6, Math.abs(c-d));\n",
       "              this.r = {x:left, y:top, w:width, h:height};\n",
       "            })\n",
       "            .attr('x', function(d) { return this.r.x })\n",
       "            .attr('y', function(d) { return this.r.y })\n",
       "            .attr('width', function(d) { return this.r.w })\n",
       "            .attr('height', function(d) { return this.r.h })\n",
       "            .filter(BrunelD3.hasData)                     // following only performed for data items\n",
       "            .style('fill', color);\n",
       "        }\n",
       "\n",
       "        // Define labeling for the selection\n",
       "        function label(selection, transitionMillis) {\n",
       "\n",
       "          var tooltipLabeling  = {\n",
       "            index: -1, method: 'box', location: ['center', 'top'], inside: true, align: 'middle', pad: 0, dy: 0.7,\n",
       "            fit: true, granularity: 0,\n",
       "            content: function(d) {\n",
       "              return d.row == null ? null : '<span class=\"title\">page_hits: </span>'\n",
       "\t\t\t+ '<span class=\"field\">' + data.page_hits_f(d) + '</span>'\n",
       "\t\t\t+ '<br/>'\n",
       "\t\t\t+ '<span class=\"title\">product: </span>'\n",
       "\t\t\t+ '<span class=\"field\">' + data.product_f(d) + '</span>'\n",
       "            }\n",
       "          };\n",
       "          BrunelD3.addTooltip(selection, tooltipLabeling, geom);\n",
       "        }\n",
       "        // Create selections, set the initial state and transition updates\n",
       "        selection = main.selectAll('.element').data(data._rows, function(d) { return d.key });\n",
       "        var added = selection.enter().append('rect');\n",
       "        merged = selection.merge(added);\n",
       "        initialState(added);\n",
       "        selection.filter(BrunelD3.hasData)\n",
       "          .classed('selected', BrunelD3.isSelected(data))\n",
       "          .filter(BrunelD3.isSelected(data)).raise();\n",
       "        updateState(BrunelD3.transition(merged, transitionMillis));\n",
       "        label(merged, transitionMillis);\n",
       "\n",
       "        BrunelD3.transition(selection.exit(), transitionMillis/3)\n",
       "          .style('opacity', 0.5).each( function() {\n",
       "            this.remove(); BrunelD3.removeLabels(this); \n",
       "        });\n",
       "        chart.select('rect.overlay')                               // attach handlers to the overlay\n",
       "          .on('click.interact', function(d) {\n",
       "            var c = BrunelD3.closest(merged, 'xy', geom.inner_radius/4 );\n",
       "            BrunelD3.select(null, this, element, updateAll);\n",
       "          });\n",
       "        merged                                                     // attach handlers to the element\n",
       "          .on('click.interact', function(d) {BrunelD3.select(d, this, element, updateAll) });\n",
       "      }\n",
       "\n",
       "      return {\n",
       "        data:           function() { return processed },\n",
       "        original:       function() { return original },\n",
       "        internal:       function() { return data },\n",
       "        selection:      function() { return merged },\n",
       "        makeData:       makeData,\n",
       "        build:          build,\n",
       "        chart:          function() { return charts[0] },\n",
       "        group:          function() { return elementGroup },\n",
       "        fields: {\n",
       "          x:            ['product'],\n",
       "          y:            ['page_hits'],\n",
       "          key:          ['product'],\n",
       "          color:        ['product']\n",
       "        }\n",
       "      };\n",
       "    }();\n",
       "\n",
       "    function build(time, noData) {\n",
       "      var first = elements[0].data() == null;\n",
       "      if (first) time = 0;                                           // no transition for first call\n",
       "      buildAxes(time);\n",
       "      if ((first || time > -1) && !noData) {\n",
       "        elements[0].makeData();\n",
       "      }\n",
       "      elements[0].build(time);\n",
       "    }\n",
       "\n",
       "    // Expose the following components of the chart\n",
       "    return {\n",
       "      elements : elements,\n",
       "      interior : interior,\n",
       "      scales: {x:scale_x, y:scale_y},\n",
       "      zoom: function(params, time) {\n",
       "          if (params) zoom.on('zoom').call(zoomNode, params, time);\n",
       "          return d3.zoomTransform(zoomNode);\n",
       "      },\n",
       "      build : build\n",
       "    };\n",
       "    }();\n",
       "\n",
       "  // Define chart #2 in the visualization //////////////////////////////////////////////////////////\n",
       "\n",
       "  charts[1] = function(parentNode, filterRows) {\n",
       "    var geom = BrunelD3.geometry(parentNode || vis.node(), 0.05, 0.6, 0.45, 1, 0, 0, 0, 0),\n",
       "      elements = [];                                              // array of elements in this chart\n",
       "\n",
       "    // Define groups for the chart parts ///////////////////////////////////////////////////////////\n",
       "\n",
       "    var chart =  vis.append('g').attr('class', 'chart2')\n",
       "      .attr('transform','translate(' + geom.chart_left + ',' + geom.chart_top + ')');\n",
       "    var overlay = chart.append('g').attr('class', 'element').attr('class', 'overlay');\n",
       "    var zoom = d3.zoom().scaleExtent([1/3,3]);\n",
       "    var zoomNode = overlay.append('rect').attr('class', 'overlay')\n",
       "      .attr('x', geom.inner_left).attr('y', geom.inner_top)\n",
       "      .attr('width', geom.inner_rawWidth).attr('height', geom.inner_rawHeight)\n",
       "      .style('cursor', 'default')\n",
       "      .node();\n",
       "    zoomNode.__zoom = d3.zoomIdentity;\n",
       "    chart.append('rect').attr('class', 'background').attr('width', geom.chart_right-geom.chart_left).attr('height', geom.chart_bottom-geom.chart_top);\n",
       "    var interior = chart.append('g').attr('class', 'interior zoomNone')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')')\n",
       "      .attr('clip-path', 'url(#clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart2_inner)');\n",
       "    interior.append('rect').attr('class', 'inner').attr('width', geom.inner_width).attr('height', geom.inner_height);\n",
       "    var gridGroup = interior.append('g').attr('class', 'grid');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart2_inner').append('rect')\n",
       "      .attr('x', 0).attr('y', 0)\n",
       "      .attr('width', geom.inner_rawWidth+1).attr('height', geom.inner_rawHeight+1);\n",
       "    var tree, expandState = [], collapseState = {};    // collapse state maps node IDs to true/false\n",
       "    var scale_x = d3.scaleLinear(), scale_y = d3.scaleLinear();\n",
       "    var base_scales = [scale_x, scale_y];                           // untransformed original scales\n",
       "    zoom.on('zoom', function(t, time) {\n",
       "        t = t || d3.event.transform;\n",
       "        zoomNode.__zoom = t;\n",
       "        interior.attr('class', 'interior ' + BrunelD3.zoomLabel(t.k));;\n",
       "        build(time || -1);\n",
       "    });\n",
       "\n",
       "    // Define element #1 ///////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    elements[0] = function() {\n",
       "      var original, processed,                           // data sets passed in and then transformed\n",
       "        element, data,                                 // brunel element information and brunel data\n",
       "        selection, merged;                                      // d3 selection and merged selection\n",
       "      var elementGroup = interior.append('g').attr('class', 'element1'),\n",
       "        main = elementGroup.append('g').attr('class', 'main'),\n",
       "        labels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'labels').attr('aria-hidden', 'true'), elementGroup),\n",
       "        diagramLabels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'diagram labels').attr('aria-hidden', 'true'), elementGroup);\n",
       "\n",
       "      function makeData() {\n",
       "        original = datasets[0];\n",
       "        if (filterRows) original = original.retainRows(filterRows);\n",
       "        processed = pre(original, 0)\n",
       "          .sort('page_hits');\n",
       "        processed = post(processed, 0);\n",
       "        var f0 = processed.field('product'),\n",
       "          f1 = processed.field('page_hits'),\n",
       "          f2 = processed.field('#selection'),\n",
       "          f3 = processed.field('#row');\n",
       "        var keyFunc = function(d) { return f0.value(d) };\n",
       "        data = {\n",
       "          product:      function(d) { return f0.value(d.row) },\n",
       "          page_hits:    function(d) { return f1.value(d.row) },\n",
       "          $selection:   function(d) { return f2.value(d.row) },\n",
       "          $row:         function(d) { return f3.value(d.row) },\n",
       "          product_f:    function(d) { return f0.valueFormatted(d.row) },\n",
       "          page_hits_f:  function(d) { return f1.valueFormatted(d.row) },\n",
       "          $selection_f: function(d) { return f2.valueFormatted(d.row) },\n",
       "          $row_f:       function(d) { return f3.valueFormatted(d.row) },\n",
       "          _split:       function(d) { return f0.value(d.row)+ '|' + f2.value(d.row)+ '|' + f1.value(d.row) },\n",
       "          _key:         keyFunc,\n",
       "          _rows:        BrunelD3.makeRowsWithKeys(keyFunc, processed.rowCount())\n",
       "        };\n",
       "      }\n",
       "      // Aesthetic Functions\n",
       "      var scale_color = d3.scaleOrdinal()\n",
       "        .domain(['A-phone', 'S-phone', 'M-phone', 'L-phone', 'H-phone', 'X-phone'])\n",
       "        .range([ '#347DAD', '#D43F58', '#F7D84A', '#31A461', '#A66A9C', '#FF954D', \n",
       "          '#A7978E', '#FFCA4D', '#F99EAF', '#B1C43B', '#7E64A2', '#FFB04D', '#CA5C7C', \n",
       "          '#DDBC8C', '#FFA28D', '#A5473D', '#8B6141', '#F57357', '#5C6B46']);\n",
       "      var color = function(d) { return scale_color(data.product(d.data)) };\n",
       "      var scale_opacity = d3.scaleOrdinal()\n",
       "        .domain(['✗', '✓'])\n",
       "        .range([0.2, 1.0]);\n",
       "      var opacity = function(d) { return scale_opacity(data.$selection(d.data)) };\n",
       "      var scale_size = d3.scaleLinear().domain([0, 21.000002])\n",
       "        .range([ 0.001, 1]);\n",
       "      var size = function(d) { return scale_size(data.page_hits(d.data)) };\n",
       "\n",
       "      // Build element from data ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "      function build(transitionMillis) {\n",
       "        element = elements[0];\n",
       "        // Define treemap (hierarchy) data structures\n",
       "        var first = (!tree), graph = BrunelData.diagram_Hierarchical.makeByNestingFields(processed, 'page_hits');\n",
       "        var targetNode = graph.root;\n",
       "        tree = d3.hierarchy(targetNode).sum(function(d) { return d.value });\n",
       "        BrunelD3.prune(tree, collapseState, true);\n",
       "        function nodeKey(d) { return d.data.key == null ? data._key(d.data.row) : d.data.key };\n",
       "        var treemap = d3.treemap().tile(d3.treemapResquarify)\n",
       "          .size([geom.inner_width, geom.inner_height])\n",
       "          .padding(function(d) { return d.depth < 3 ? 2*d.depth : 0} )\n",
       "          .paddingTop(function(d) { return d.depth ==1 ? 15 : (d.depth == 2) ? 12 : 0});\n",
       "        main.attr('class', 'diagram treemap hierarchy');\n",
       "        var labeling  = [{\n",
       "          index: 0, method: 'box', location: ['center', 'center'], inside: true, align: 'middle', pad: 3, dy: 0.3,\n",
       "          fit: true, granularity: 1,\n",
       "          content: function(d) {\n",
       "            return d.row == null ? null : data.product_f(d)\n",
       "          }\n",
       "        }];\n",
       "\n",
       "        // Define selection entry operations\n",
       "        function initialState(selection) {\n",
       "          selection\n",
       "            .attr('class', 'element polygon filled')\n",
       "            .filter(function(d) { return d.parent })\n",
       "            .attr('x', function(d) { return scale_x((d.parent.x0+d.parent.x1)/2) })\n",
       "            .attr('y', function(d) { return scale_y((d.parent.y0+d.parent.y1)/2) })\n",
       "            .attr('width', 0).attr('height', 0)\n",
       "        }\n",
       "\n",
       "        // Define selection update operations on merged data\n",
       "        function updateState(selection) {\n",
       "          selection\n",
       "            .attr('class', function(d) { return (d.collapsed ? 'collapsed ' : '') + (d.data.children ? 'element L' + d.depth : 'leaf element bar') })\n",
       "            .attr('x', function(d) { return scale_x(d.x0) })\n",
       "            .attr('y', function(d) { return scale_y(d.y0) })\n",
       "            .attr('width', function(d) { return scale_x(d.x1) - scale_x(d.x0) })\n",
       "            .attr('height', function(d) { return scale_y(d.y1) - scale_y(d.y0) })\n",
       "            .filter(BrunelD3.hasData)                     // following only performed for data items\n",
       "            .style('fill', color)\n",
       "            .style('fill-opacity', opacity)\n",
       "            .style('stroke-opacity', opacity);\n",
       "        }\n",
       "\n",
       "        // Define labeling for the selection\n",
       "        function label(selection, transitionMillis) {\n",
       "          BrunelD3.label(selection, labels, transitionMillis, geom, labeling);\n",
       "\n",
       "          var tooltipLabeling  = {\n",
       "            index: -1, method: 'box', location: ['center', 'top'], inside: true, align: 'middle', pad: 0, dy: 0.7,\n",
       "            fit: true, granularity: 0,\n",
       "            content: function(d) {\n",
       "              return d.row == null ? null : 'page views: '\n",
       "\t\t\t+ '<span class=\"field\">' + data.page_hits_f(d) + '</span>'\n",
       "            }\n",
       "          };\n",
       "          BrunelD3.addTooltip(selection, tooltipLabeling, geom);\n",
       "          diagramLabels.attr('class', 'axis diagram treemap hierarchy');\n",
       "          var treeLabeling = [{ index:0, method:'inner-left', fit:true, dy:0.83, align:'start', \n",
       "            content:  function(d) { return d.data.innerNodeName },\n",
       "            cssClass: function(d) { return 'axis label L' + d.depth + ' H' + d.height }, \n",
       "            where :   function(box) { return {'x': box.x + 2, 'y': box.y, 'box': box} }\n",
       "          }];\n",
       "          BrunelD3.label(selection.filter(function(d) {return d.data.key}), diagramLabels, transitionMillis, geom, treeLabeling);\n",
       "        }\n",
       "        // Create selections, set the initial state and transition updates\n",
       "        selection = main.selectAll('.element').data(treemap(tree).descendants(), nodeKey);\n",
       "        var added = selection.enter().append('rect');\n",
       "        merged = selection.merge(added);\n",
       "        initialState(added);\n",
       "        selection.filter(BrunelD3.hasData)\n",
       "          .classed('selected', BrunelD3.isSelected(data))\n",
       "          .filter(BrunelD3.isSelected(data)).raise();\n",
       "        updateState(BrunelD3.transition(merged, transitionMillis));\n",
       "        label(merged, transitionMillis);\n",
       "\n",
       "        BrunelD3.transition(selection.exit(), transitionMillis/3)\n",
       "          .style('opacity', 0.5).each( function() {\n",
       "            this.remove(); BrunelD3.removeLabels(this); \n",
       "        });\n",
       "        merged                                                     // attach handlers to the element\n",
       "          .on('dblclick.collapse', function(d) {if (d.data.children) {collapseState[d.data.key] = !collapseState[d.data.key]; charts[1].build(500)}  });\n",
       "      }\n",
       "\n",
       "      return {\n",
       "        data:           function() { return processed },\n",
       "        original:       function() { return original },\n",
       "        internal:       function() { return data },\n",
       "        selection:      function() { return merged },\n",
       "        makeData:       makeData,\n",
       "        build:          build,\n",
       "        chart:          function() { return charts[1] },\n",
       "        group:          function() { return elementGroup },\n",
       "        fields: {\n",
       "          key:          ['product'],\n",
       "          color:        ['product'],\n",
       "          size:         ['page_hits'],\n",
       "          opacity:      ['#selection']\n",
       "        }\n",
       "      };\n",
       "    }();\n",
       "\n",
       "    function build(time, noData) {\n",
       "      var first = elements[0].data() == null;\n",
       "      if (first) time = 0;                                           // no transition for first call\n",
       "      if ((first || time > -1) && !noData) {\n",
       "        elements[0].makeData();\n",
       "      }\n",
       "      elements[0].build(time);\n",
       "    }\n",
       "\n",
       "    // Expose the following components of the chart\n",
       "    return {\n",
       "      elements : elements,\n",
       "      interior : interior,\n",
       "      zoom: function(params, time) {\n",
       "          if (params) zoom.on('zoom').call(zoomNode, params, time);\n",
       "          return d3.zoomTransform(zoomNode);\n",
       "      },\n",
       "      build : build\n",
       "    };\n",
       "    }();\n",
       "\n",
       "  // Define chart #3 in the visualization //////////////////////////////////////////////////////////\n",
       "\n",
       "  charts[2] = function(parentNode, filterRows) {\n",
       "    var geom = BrunelD3.geometry(parentNode || vis.node(), 0.5, 0, 1, 0.5, 22, 49, 37, 0),\n",
       "      elements = [];                                              // array of elements in this chart\n",
       "\n",
       "    // Define groups for the chart parts ///////////////////////////////////////////////////////////\n",
       "\n",
       "    var chart =  vis.append('g').attr('class', 'chart3')\n",
       "      .attr('transform','translate(' + geom.chart_left + ',' + geom.chart_top + ')');\n",
       "    var overlay = chart.append('g').attr('class', 'element').attr('class', 'overlay');\n",
       "    var zoom = d3.zoom().scaleExtent([1/3,3]);\n",
       "    var zoomNode = overlay.append('rect').attr('class', 'overlay')\n",
       "      .attr('x', geom.inner_left).attr('y', geom.inner_top)\n",
       "      .attr('width', geom.inner_rawWidth).attr('height', geom.inner_rawHeight)\n",
       "      .style('cursor', 'move').call(zoom)\n",
       "      .node();\n",
       "    zoomNode.__zoom = d3.zoomIdentity;\n",
       "    chart.append('rect').attr('class', 'background').attr('width', geom.chart_right-geom.chart_left).attr('height', geom.chart_bottom-geom.chart_top);\n",
       "    var interior = chart.append('g').attr('class', 'interior zoomNone')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')')\n",
       "      .attr('clip-path', 'url(#clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart3_inner)');\n",
       "    interior.append('rect').attr('class', 'inner').attr('width', geom.inner_width).attr('height', geom.inner_height);\n",
       "    var gridGroup = interior.append('g').attr('class', 'grid');\n",
       "    var axes = chart.append('g').attr('class', 'axis')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart3_inner').append('rect')\n",
       "      .attr('x', 0).attr('y', 0)\n",
       "      .attr('width', geom.inner_rawWidth+1).attr('height', geom.inner_rawHeight+1);\n",
       "    chart.append('text').attr('class', 'title header').text('Total Time by Smart Phone').style('text-anchor', 'middle')\n",
       "      .attr('x','50%')\n",
       "      .attr('y',2).attr('dy','0.8em');\n",
       "\n",
       "    // Scales //////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    var scale_x = d3.scalePoint().padding(0.5)\n",
       "      .domain(['A-phone', 'S-phone', 'M-phone', 'L-phone', 'H-phone', 'X-phone'])\n",
       "      .range([0, geom.inner_width]);\n",
       "    var scale_inner = d3.scaleLinear().domain([0,1])\n",
       "      .range([-0.5, 0.5]);\n",
       "    var scale_y = d3.scaleLinear().domain([0, 2000.0002])\n",
       "      .range([geom.inner_height, 0]);\n",
       "    var base_scales = [scale_x, scale_y];                           // untransformed original scales\n",
       "\n",
       "    // Axes ////////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    axes.append('g').attr('class', 'x axis')\n",
       "      .attr('transform','translate(0,' + geom.inner_rawHeight + ')')\n",
       "      .attr('clip-path', 'url(#clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart3_haxis)');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart3_haxis').append('polyline')\n",
       "      .attr('points', '-1,-1000, -1,-1 -5,5, -1000,5, -100,1000, 10000,1000 10000,-1000');\n",
       "    axes.select('g.axis.x').append('text').attr('class', 'title').text('product').style('text-anchor', 'middle')\n",
       "      .attr('x',geom.inner_rawWidth/2)\n",
       "      .attr('y', geom.inner_bottom - 2.0).attr('dy','-0.27em');\n",
       "    axes.append('g').attr('class', 'y axis')\n",
       "      .attr('clip-path', 'url(#clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart3_vaxis)');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart3_vaxis').append('polyline')\n",
       "      .attr('points', '-1000,-10000, 10000,-10000, 10000,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+5) + ', -1000,' + (geom.inner_rawHeight+5) );\n",
       "    axes.select('g.axis.y').append('text').attr('class', 'title').text('total_time').style('text-anchor', 'middle')\n",
       "      .attr('x',-geom.inner_rawHeight/2)\n",
       "      .attr('y', 4-geom.inner_left).attr('dy', '0.7em').attr('transform', 'rotate(270)');\n",
       "\n",
       "    var axis_bottom = d3.axisBottom(scale_x).ticks(Math.min(10, Math.round(geom.inner_width / 66.0)));\n",
       "    var axis_left = d3.axisLeft(scale_y).ticks(Math.min(10, Math.round(geom.inner_width / 20)));\n",
       "\n",
       "    function buildAxes(time) {\n",
       "      axis_bottom.tickValues(BrunelD3.filterTicks(scale_x))\n",
       "      var axis_x = axes.select('g.axis.x');\n",
       "      BrunelD3.transition(axis_x, time).call(axis_bottom.scale(scale_x));\n",
       "      var axis_y = axes.select('g.axis.y');\n",
       "      BrunelD3.transition(axis_y, time).call(axis_left.scale(scale_y));\n",
       "    }\n",
       "    zoom.on('zoom', function(t, time) {\n",
       "        t = t ||BrunelD3.restrictZoom(d3.event.transform, geom, this);\n",
       "        scale_y = t.rescaleY(base_scales[1]);\n",
       "        zoomNode.__zoom = t;\n",
       "        interior.attr('class', 'interior ' + BrunelD3.zoomLabel(t.k));;\n",
       "        build(time || -1);\n",
       "    });\n",
       "\n",
       "    // Define element #1 ///////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    elements[0] = function() {\n",
       "      var original, processed,                           // data sets passed in and then transformed\n",
       "        element, data,                                 // brunel element information and brunel data\n",
       "        selection, merged;                                      // d3 selection and merged selection\n",
       "      var elementGroup = interior.append('g').attr('class', 'element1'),\n",
       "        main = elementGroup.append('g').attr('class', 'main'),\n",
       "        labels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'labels').attr('aria-hidden', 'true'), elementGroup);\n",
       "\n",
       "      function makeData() {\n",
       "        original = datasets[0];\n",
       "        if (filterRows) original = original.retainRows(filterRows);\n",
       "        processed = pre(original, 0)\n",
       "          .sort('page_hits');\n",
       "        processed = post(processed, 0);\n",
       "        var f0 = processed.field('product'),\n",
       "          f1 = processed.field('total_time'),\n",
       "          f2 = processed.field('page_hits'),\n",
       "          f3 = processed.field('#row'),\n",
       "          f4 = processed.field('#selection');\n",
       "        var keyFunc = function(d) { return f0.value(d) };\n",
       "        data = {\n",
       "          product:      function(d) { return f0.value(d.row) },\n",
       "          total_time:   function(d) { return f1.value(d.row) },\n",
       "          page_hits:    function(d) { return f2.value(d.row) },\n",
       "          $row:         function(d) { return f3.value(d.row) },\n",
       "          $selection:   function(d) { return f4.value(d.row) },\n",
       "          product_f:    function(d) { return f0.valueFormatted(d.row) },\n",
       "          total_time_f: function(d) { return f1.valueFormatted(d.row) },\n",
       "          page_hits_f:  function(d) { return f2.valueFormatted(d.row) },\n",
       "          $row_f:       function(d) { return f3.valueFormatted(d.row) },\n",
       "          $selection_f: function(d) { return f4.valueFormatted(d.row) },\n",
       "          _split:       function(d) { return f0.value(d.row) },\n",
       "          _key:         keyFunc,\n",
       "          _rows:        BrunelD3.makeRowsWithKeys(keyFunc, processed.rowCount())\n",
       "        };\n",
       "      }\n",
       "      // Aesthetic Functions\n",
       "      var scale_color = d3.scaleOrdinal()\n",
       "        .domain(['A-phone', 'S-phone', 'M-phone', 'L-phone', 'H-phone', 'X-phone'])\n",
       "        .range([ '#347DAD', '#D43F58', '#F7D84A', '#31A461', '#A66A9C', '#FF954D', \n",
       "          '#A7978E', '#FFCA4D', '#F99EAF', '#B1C43B', '#7E64A2', '#FFB04D', '#CA5C7C', \n",
       "          '#DDBC8C', '#FFA28D', '#A5473D', '#8B6141', '#F57357', '#5C6B46']);\n",
       "      var color = function(d) { return scale_color(data.product(d)) };\n",
       "\n",
       "      // Build element from data ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "      function build(transitionMillis) {\n",
       "        element = elements[0];\n",
       "        var w = 0.9 * Math.abs(scale_x(scale_x.domain()[1]) - scale_x(scale_x.domain()[0]) );\n",
       "        var x = function(d) { return scale_x(data.product(d))};\n",
       "        var h = Math.abs( scale_y(scale_y.domain()[0] + 60.0) - scale_y.range()[0] );\n",
       "        var y1 = scale_y.range()[0];\n",
       "        var y2 = function(d) { return scale_y(data.total_time(d))};\n",
       "        var labeling  = [{\n",
       "          index: 0, method: 'box', location: ['center', 'center'], inside: true, align: 'middle', pad: 3, dy: 0.3,\n",
       "          fit: true, granularity: 1,\n",
       "          content: function(d) {\n",
       "            return d.row == null ? null : data.product_f(d)\n",
       "          }\n",
       "        }];\n",
       "\n",
       "        // Define selection entry operations\n",
       "        function initialState(selection) {\n",
       "          selection\n",
       "            .attr('class', 'element bar filled')\n",
       "        }\n",
       "\n",
       "        // Define selection update operations on merged data\n",
       "        function updateState(selection) {\n",
       "          selection\n",
       "            .each(function(d) {\n",
       "              var width = w, left = x(d) - width/2, \n",
       "              c = y1, d = y2(d), top = Math.min(c,d), height = Math.max(1e-6, Math.abs(c-d));\n",
       "              this.r = {x:left, y:top, w:width, h:height};\n",
       "            })\n",
       "            .attr('x', function(d) { return this.r.x })\n",
       "            .attr('y', function(d) { return this.r.y })\n",
       "            .attr('width', function(d) { return this.r.w })\n",
       "            .attr('height', function(d) { return this.r.h })\n",
       "            .filter(BrunelD3.hasData)                     // following only performed for data items\n",
       "            .style('fill', color);\n",
       "        }\n",
       "\n",
       "        // Define labeling for the selection\n",
       "        function label(selection, transitionMillis) {\n",
       "          BrunelD3.label(selection, labels, transitionMillis, geom, labeling);\n",
       "\n",
       "          var tooltipLabeling  = {\n",
       "            index: -1, method: 'box', location: ['center', 'top'], inside: true, align: 'middle', pad: 0, dy: 0.7,\n",
       "            fit: true, granularity: 0,\n",
       "            content: function(d) {\n",
       "              return d.row == null ? null : 'time on page (sec): '\n",
       "\t\t\t+ '<span class=\"field\">' + data.total_time_f(d) + '</span>'\n",
       "            }\n",
       "          };\n",
       "          BrunelD3.addTooltip(selection, tooltipLabeling, geom);\n",
       "        }\n",
       "        // Create selections, set the initial state and transition updates\n",
       "        selection = main.selectAll('.element').data(data._rows, function(d) { return d.key });\n",
       "        var added = selection.enter().append('rect');\n",
       "        merged = selection.merge(added);\n",
       "        initialState(added);\n",
       "        selection.filter(BrunelD3.hasData)\n",
       "          .classed('selected', BrunelD3.isSelected(data))\n",
       "          .filter(BrunelD3.isSelected(data)).raise();\n",
       "        updateState(BrunelD3.transition(merged, transitionMillis));\n",
       "        label(merged, transitionMillis);\n",
       "\n",
       "        BrunelD3.transition(selection.exit(), transitionMillis/3)\n",
       "          .style('opacity', 0.5).each( function() {\n",
       "            this.remove(); BrunelD3.removeLabels(this); \n",
       "        });\n",
       "        chart.select('rect.overlay')                               // attach handlers to the overlay\n",
       "          .on('click.interact', function(d) {\n",
       "            var c = BrunelD3.closest(merged, 'xy', geom.inner_radius/4 );\n",
       "            BrunelD3.select(null, this, element, updateAll);\n",
       "          });\n",
       "        merged                                                     // attach handlers to the element\n",
       "          .on('click.interact', function(d) {BrunelD3.select(d, this, element, updateAll) });\n",
       "      }\n",
       "\n",
       "      return {\n",
       "        data:           function() { return processed },\n",
       "        original:       function() { return original },\n",
       "        internal:       function() { return data },\n",
       "        selection:      function() { return merged },\n",
       "        makeData:       makeData,\n",
       "        build:          build,\n",
       "        chart:          function() { return charts[2] },\n",
       "        group:          function() { return elementGroup },\n",
       "        fields: {\n",
       "          x:            ['product'],\n",
       "          y:            ['total_time'],\n",
       "          key:          ['product'],\n",
       "          color:        ['product']\n",
       "        }\n",
       "      };\n",
       "    }();\n",
       "\n",
       "    function build(time, noData) {\n",
       "      var first = elements[0].data() == null;\n",
       "      if (first) time = 0;                                           // no transition for first call\n",
       "      buildAxes(time);\n",
       "      if ((first || time > -1) && !noData) {\n",
       "        elements[0].makeData();\n",
       "      }\n",
       "      elements[0].build(time);\n",
       "    }\n",
       "\n",
       "    // Expose the following components of the chart\n",
       "    return {\n",
       "      elements : elements,\n",
       "      interior : interior,\n",
       "      scales: {x:scale_x, y:scale_y},\n",
       "      zoom: function(params, time) {\n",
       "          if (params) zoom.on('zoom').call(zoomNode, params, time);\n",
       "          return d3.zoomTransform(zoomNode);\n",
       "      },\n",
       "      build : build\n",
       "    };\n",
       "    }();\n",
       "\n",
       "  // Define chart #4 in the visualization //////////////////////////////////////////////////////////\n",
       "\n",
       "  charts[3] = function(parentNode, filterRows) {\n",
       "    var geom = BrunelD3.geometry(parentNode || vis.node(), 0.55, 0.6, 0.95, 1, 0, 0, 0, 0),\n",
       "      elements = [];                                              // array of elements in this chart\n",
       "\n",
       "    // Define groups for the chart parts ///////////////////////////////////////////////////////////\n",
       "\n",
       "    var chart =  vis.append('g').attr('class', 'chart4')\n",
       "      .attr('transform','translate(' + geom.chart_left + ',' + geom.chart_top + ')');\n",
       "    var overlay = chart.append('g').attr('class', 'element').attr('class', 'overlay');\n",
       "    var zoom = d3.zoom().scaleExtent([1/3,3]);\n",
       "    var zoomNode = overlay.append('rect').attr('class', 'overlay')\n",
       "      .attr('x', geom.inner_left).attr('y', geom.inner_top)\n",
       "      .attr('width', geom.inner_rawWidth).attr('height', geom.inner_rawHeight)\n",
       "      .style('cursor', 'default')\n",
       "      .node();\n",
       "    zoomNode.__zoom = d3.zoomIdentity;\n",
       "    chart.append('rect').attr('class', 'background').attr('width', geom.chart_right-geom.chart_left).attr('height', geom.chart_bottom-geom.chart_top);\n",
       "    var interior = chart.append('g').attr('class', 'interior zoomNone')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')')\n",
       "      .attr('clip-path', 'url(#clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart4_inner)');\n",
       "    interior.append('rect').attr('class', 'inner').attr('width', geom.inner_width).attr('height', geom.inner_height);\n",
       "    var gridGroup = interior.append('g').attr('class', 'grid');\n",
       "    vis.append('clipPath').attr('id', 'clip_visidac89e56a-7c13-4871-bd78-ce10c7429f4a_chart4_inner').append('rect')\n",
       "      .attr('x', 0).attr('y', 0)\n",
       "      .attr('width', geom.inner_rawWidth+1).attr('height', geom.inner_rawHeight+1);\n",
       "    var tree, expandState = [], collapseState = {};    // collapse state maps node IDs to true/false\n",
       "    var scale_x = d3.scaleLinear(), scale_y = d3.scaleLinear();\n",
       "    var base_scales = [scale_x, scale_y];                           // untransformed original scales\n",
       "    zoom.on('zoom', function(t, time) {\n",
       "        t = t || d3.event.transform;\n",
       "        zoomNode.__zoom = t;\n",
       "        interior.attr('class', 'interior ' + BrunelD3.zoomLabel(t.k));;\n",
       "        build(time || -1);\n",
       "    });\n",
       "\n",
       "    // Define element #1 ///////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    elements[0] = function() {\n",
       "      var original, processed,                           // data sets passed in and then transformed\n",
       "        element, data,                                 // brunel element information and brunel data\n",
       "        selection, merged;                                      // d3 selection and merged selection\n",
       "      var elementGroup = interior.append('g').attr('class', 'element1'),\n",
       "        main = elementGroup.append('g').attr('class', 'main'),\n",
       "        labels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'labels').attr('aria-hidden', 'true'), elementGroup),\n",
       "        diagramLabels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'diagram labels').attr('aria-hidden', 'true'), elementGroup);\n",
       "\n",
       "      function makeData() {\n",
       "        original = datasets[0];\n",
       "        if (filterRows) original = original.retainRows(filterRows);\n",
       "        processed = pre(original, 0)\n",
       "          .sort('page_hits');\n",
       "        processed = post(processed, 0);\n",
       "        var f0 = processed.field('product'),\n",
       "          f1 = processed.field('total_time'),\n",
       "          f2 = processed.field('#selection'),\n",
       "          f3 = processed.field('page_hits'),\n",
       "          f4 = processed.field('#row');\n",
       "        var keyFunc = function(d) { return f0.value(d) };\n",
       "        data = {\n",
       "          product:      function(d) { return f0.value(d.row) },\n",
       "          total_time:   function(d) { return f1.value(d.row) },\n",
       "          $selection:   function(d) { return f2.value(d.row) },\n",
       "          page_hits:    function(d) { return f3.value(d.row) },\n",
       "          $row:         function(d) { return f4.value(d.row) },\n",
       "          product_f:    function(d) { return f0.valueFormatted(d.row) },\n",
       "          total_time_f: function(d) { return f1.valueFormatted(d.row) },\n",
       "          $selection_f: function(d) { return f2.valueFormatted(d.row) },\n",
       "          page_hits_f:  function(d) { return f3.valueFormatted(d.row) },\n",
       "          $row_f:       function(d) { return f4.valueFormatted(d.row) },\n",
       "          _split:       function(d) { return f0.value(d.row)+ '|' + f2.value(d.row)+ '|' + f1.value(d.row) },\n",
       "          _key:         keyFunc,\n",
       "          _rows:        BrunelD3.makeRowsWithKeys(keyFunc, processed.rowCount())\n",
       "        };\n",
       "      }\n",
       "      // Aesthetic Functions\n",
       "      var scale_color = d3.scaleOrdinal()\n",
       "        .domain(['A-phone', 'S-phone', 'M-phone', 'L-phone', 'H-phone', 'X-phone'])\n",
       "        .range([ '#347DAD', '#D43F58', '#F7D84A', '#31A461', '#A66A9C', '#FF954D', \n",
       "          '#A7978E', '#FFCA4D', '#F99EAF', '#B1C43B', '#7E64A2', '#FFB04D', '#CA5C7C', \n",
       "          '#DDBC8C', '#FFA28D', '#A5473D', '#8B6141', '#F57357', '#5C6B46']);\n",
       "      var color = function(d) { return scale_color(data.product(d.data)) };\n",
       "      var scale_opacity = d3.scaleOrdinal()\n",
       "        .domain(['✗', '✓'])\n",
       "        .range([0.2, 1.0]);\n",
       "      var opacity = function(d) { return scale_opacity(data.$selection(d.data)) };\n",
       "      var scale_size = d3.scaleLinear().domain([0, 1920.0002])\n",
       "        .range([ 0.001, 1]);\n",
       "      var size = function(d) { return scale_size(data.total_time(d.data)) };\n",
       "\n",
       "      // Build element from data ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "      function build(transitionMillis) {\n",
       "        element = elements[0];\n",
       "        // Define treemap (hierarchy) data structures\n",
       "        var first = (!tree), graph = BrunelData.diagram_Hierarchical.makeByNestingFields(processed, 'total_time');\n",
       "        var targetNode = graph.root;\n",
       "        tree = d3.hierarchy(targetNode).sum(function(d) { return d.value });\n",
       "        BrunelD3.prune(tree, collapseState, true);\n",
       "        function nodeKey(d) { return d.data.key == null ? data._key(d.data.row) : d.data.key };\n",
       "        var treemap = d3.treemap().tile(d3.treemapResquarify)\n",
       "          .size([geom.inner_width, geom.inner_height])\n",
       "          .padding(function(d) { return d.depth < 3 ? 2*d.depth : 0} )\n",
       "          .paddingTop(function(d) { return d.depth ==1 ? 15 : (d.depth == 2) ? 12 : 0});\n",
       "        main.attr('class', 'diagram treemap hierarchy');\n",
       "        var labeling  = [{\n",
       "          index: 0, method: 'box', location: ['center', 'center'], inside: true, align: 'middle', pad: 3, dy: 0.3,\n",
       "          fit: true, granularity: 1,\n",
       "          content: function(d) {\n",
       "            return d.row == null ? null : data.product_f(d)\n",
       "          }\n",
       "        }];\n",
       "\n",
       "        // Define selection entry operations\n",
       "        function initialState(selection) {\n",
       "          selection\n",
       "            .attr('class', 'element polygon filled')\n",
       "            .filter(function(d) { return d.parent })\n",
       "            .attr('x', function(d) { return scale_x((d.parent.x0+d.parent.x1)/2) })\n",
       "            .attr('y', function(d) { return scale_y((d.parent.y0+d.parent.y1)/2) })\n",
       "            .attr('width', 0).attr('height', 0)\n",
       "        }\n",
       "\n",
       "        // Define selection update operations on merged data\n",
       "        function updateState(selection) {\n",
       "          selection\n",
       "            .attr('class', function(d) { return (d.collapsed ? 'collapsed ' : '') + (d.data.children ? 'element L' + d.depth : 'leaf element bar') })\n",
       "            .attr('x', function(d) { return scale_x(d.x0) })\n",
       "            .attr('y', function(d) { return scale_y(d.y0) })\n",
       "            .attr('width', function(d) { return scale_x(d.x1) - scale_x(d.x0) })\n",
       "            .attr('height', function(d) { return scale_y(d.y1) - scale_y(d.y0) })\n",
       "            .filter(BrunelD3.hasData)                     // following only performed for data items\n",
       "            .style('fill', color)\n",
       "            .style('fill-opacity', opacity)\n",
       "            .style('stroke-opacity', opacity);\n",
       "        }\n",
       "\n",
       "        // Define labeling for the selection\n",
       "        function label(selection, transitionMillis) {\n",
       "          BrunelD3.label(selection, labels, transitionMillis, geom, labeling);\n",
       "\n",
       "          var tooltipLabeling  = {\n",
       "            index: -1, method: 'box', location: ['center', 'top'], inside: true, align: 'middle', pad: 0, dy: 0.7,\n",
       "            fit: true, granularity: 0,\n",
       "            content: function(d) {\n",
       "              return d.row == null ? null : 'time on page (sec): '\n",
       "\t\t\t+ '<span class=\"field\">' + data.total_time_f(d) + '</span>'\n",
       "            }\n",
       "          };\n",
       "          BrunelD3.addTooltip(selection, tooltipLabeling, geom);\n",
       "          diagramLabels.attr('class', 'axis diagram treemap hierarchy');\n",
       "          var treeLabeling = [{ index:0, method:'inner-left', fit:true, dy:0.83, align:'start', \n",
       "            content:  function(d) { return d.data.innerNodeName },\n",
       "            cssClass: function(d) { return 'axis label L' + d.depth + ' H' + d.height }, \n",
       "            where :   function(box) { return {'x': box.x + 2, 'y': box.y, 'box': box} }\n",
       "          }];\n",
       "          BrunelD3.label(selection.filter(function(d) {return d.data.key}), diagramLabels, transitionMillis, geom, treeLabeling);\n",
       "        }\n",
       "        // Create selections, set the initial state and transition updates\n",
       "        selection = main.selectAll('.element').data(treemap(tree).descendants(), nodeKey);\n",
       "        var added = selection.enter().append('rect');\n",
       "        merged = selection.merge(added);\n",
       "        initialState(added);\n",
       "        selection.filter(BrunelD3.hasData)\n",
       "          .classed('selected', BrunelD3.isSelected(data))\n",
       "          .filter(BrunelD3.isSelected(data)).raise();\n",
       "        updateState(BrunelD3.transition(merged, transitionMillis));\n",
       "        label(merged, transitionMillis);\n",
       "\n",
       "        BrunelD3.transition(selection.exit(), transitionMillis/3)\n",
       "          .style('opacity', 0.5).each( function() {\n",
       "            this.remove(); BrunelD3.removeLabels(this); \n",
       "        });\n",
       "        merged                                                     // attach handlers to the element\n",
       "          .on('dblclick.collapse', function(d) {if (d.data.children) {collapseState[d.data.key] = !collapseState[d.data.key]; charts[3].build(500)}  });\n",
       "      }\n",
       "\n",
       "      return {\n",
       "        data:           function() { return processed },\n",
       "        original:       function() { return original },\n",
       "        internal:       function() { return data },\n",
       "        selection:      function() { return merged },\n",
       "        makeData:       makeData,\n",
       "        build:          build,\n",
       "        chart:          function() { return charts[3] },\n",
       "        group:          function() { return elementGroup },\n",
       "        fields: {\n",
       "          key:          ['product'],\n",
       "          color:        ['product'],\n",
       "          size:         ['total_time'],\n",
       "          opacity:      ['#selection']\n",
       "        }\n",
       "      };\n",
       "    }();\n",
       "\n",
       "    function build(time, noData) {\n",
       "      var first = elements[0].data() == null;\n",
       "      if (first) time = 0;                                           // no transition for first call\n",
       "      if ((first || time > -1) && !noData) {\n",
       "        elements[0].makeData();\n",
       "      }\n",
       "      elements[0].build(time);\n",
       "    }\n",
       "\n",
       "    // Expose the following components of the chart\n",
       "    return {\n",
       "      elements : elements,\n",
       "      interior : interior,\n",
       "      zoom: function(params, time) {\n",
       "          if (params) zoom.on('zoom').call(zoomNode, params, time);\n",
       "          return d3.zoomTransform(zoomNode);\n",
       "      },\n",
       "      build : build\n",
       "    };\n",
       "    }();\n",
       "\n",
       "  function setData(rowData, i) { datasets[i||0] = BrunelD3.makeData(rowData) }\n",
       "  function updateAll(time) { charts.forEach(function(x) {x.build(time || 0)}) }\n",
       "  function buildAll() {\n",
       "    for (var i=0;i<arguments.length;i++) setData(arguments[i], i);\n",
       "    updateAll(transitionTime);\n",
       "  }\n",
       "\n",
       "  return {\n",
       "    dataPreProcess:     function(f) { if (f) pre = f; return pre },\n",
       "    dataPostProcess:    function(f) { if (f) post = f; return post },\n",
       "    data:               function(d,i) { if (d) setData(d,i); return datasets[i||0] },\n",
       "    visId:              visId,\n",
       "    build:              buildAll,\n",
       "    rebuild:            updateAll,\n",
       "    charts:             charts\n",
       "  }\n",
       "}\n",
       "\n",
       "// Data Tables /////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "var table1 = {\n",
       "   summarized: false,\n",
       "   names: ['product', 'page_hits', 'total_time'], \n",
       "   options: ['string', 'numeric', 'numeric'], \n",
       "   rows: [['A-phone', 21, 1920], ['S-phone', 12, 1380], ['M-phone', 5, 540], ['L-phone', 3, 180],\n",
       "  ['H-phone', 2, 120], ['X-phone', 1, 720]]\n",
       "};\n",
       "\n",
       "// Call Code to Build the system ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "var v = new BrunelVis('visidac89e56a-7c13-4871-bd78-ce10c7429f4a');\n",
       "v .build(table1);\n",
       "\n",
       "            \"\"\n",
       "        });\n",
       "        });\n",
       "        </script>"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "%%brunel data('productMetrics') \n",
    "bar at(0,0,50,50) title(\"Page Views by Smart Phone\")\n",
    "  x(product) y(page_hits)\n",
    "  tooltip(page_hits,product) color(product) legends(none)\n",
    "  axes(x:'smart phones',y:'page views') sort(page_hits) interaction (select)|\n",
    "treemap at(60,5,100,45)\n",
    "  sort(page_hits) size(page_hits) color(product) label(product) legends(none)\n",
    "  tooltip(\"page views: \",page_hits) opacity(#selection) | \n",
    "bar at(0,50,50,100)\n",
    "  title(\"Total Time by Smart Phone\")\n",
    "  x(product) y(total_time)\n",
    "  color(product) label(product) tooltip(\"time on page (sec): \",total_time)\n",
    "  legends(none) sort(page_hits) interaction(select)|\n",
    "treemap at(60,55,100,95)\n",
    "  sort(page_hits) size(total_time) color(product) label(product) legends(none)\n",
    "  tooltip(\"time on page (sec): \",total_time) opacity(#selection)\n",
    " :: width=1000, height=600"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Aggregated web metrics for smart phone features "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "collapsed": false,
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+-------+---------------+---------+----------+\n",
      "|product|        feature|page_hits|total_time|\n",
      "+-------+---------------+---------+----------+\n",
      "|A-phone|          color|        5|       660|\n",
      "|A-phone|        battery|        1|       120|\n",
      "|A-phone|      processor|        2|       120|\n",
      "|A-phone|voice_assistant|        2|       120|\n",
      "|A-phone|         camera|        3|       180|\n",
      "+-------+---------------+---------+----------+\n",
      "\n"
     ]
    }
   ],
   "source": [
    "val featureMetrics = webMetricsDF.\n",
    "  select(\"product\", \"feature\", \"page_hits\", \"total_time\").\n",
    "  filter($\"action\" === \"details\").\n",
    "  filter($\"product\" === \"A-phone\").\n",
    "  filter(\"feature != ''\").\n",
    "  groupBy(\"product\",\"feature\").agg(sum(\"page_hits\"), sum(\"total_time\")).\n",
    "  withColumnRenamed(\"sum(page_hits)\",\"page_hits\").\n",
    "  withColumnRenamed(\"sum(total_time)\",\"total_time\")\n",
    "\n",
    "featureMetrics.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "collapsed": false,
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "         <link rel=\"stylesheet\" type=\"text/css\" href=\"https://brunelvis.org/js/brunel.2.3.css\" charset=\"utf-8\">\n",
       "         <link rel=\"stylesheet\" type=\"text/css\" href=\"https://brunelvis.org/js/sumoselect.css\" charset=\"utf-8\">\n",
       "         <style>  </style>\n",
       "         <div id=\"controlsIde5d97c2e-be5b-4b30-82b3-efc7d6042280\" class=\"brunel\"/>\n",
       "<svg id=\"visid3a305647-bed8-4d21-8a22-9fb6811949c8\" width=\"1000\" height=\"300\"></svg>\n",
       "\n",
       "<script>\n",
       "require.config({\n",
       "            waitSeconds: 60,\n",
       "            paths: {\n",
       "                'd3': '//cdnjs.cloudflare.com/ajax/libs/d3/4.2.1/d3.min',\n",
       "                'topojson' : '//cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min',\n",
       "                'brunel' : 'https://brunelvis.org/js/brunel.2.3.min',\n",
       "                'brunelControls' : 'https://brunelvis.org/js/brunel.controls.2.3.min'\n",
       "            },\n",
       "\n",
       "            shim: {\n",
       "               'brunel' : {\n",
       "                    exports: 'BrunelD3',\n",
       "                    deps: ['d3', 'topojson'],\n",
       "                    init: function() {\n",
       "                       return {\n",
       "                         BrunelD3 : BrunelD3,\n",
       "                         BrunelData : BrunelData\n",
       "                      }\n",
       "                    }\n",
       "                },\n",
       "               'brunelControls' : {\n",
       "                    exports: 'BrunelEventHandlers',\n",
       "                    init: function() {\n",
       "                       return {\n",
       "                         BrunelEventHandlers: BrunelEventHandlers,\n",
       "                         BrunelJQueryControlFactory: BrunelJQueryControlFactory\n",
       "                      }\n",
       "                    }\n",
       "                }\n",
       "\n",
       "            }\n",
       "\n",
       "        });\n",
       "\n",
       "        require([\"d3\"], function(d3) {\n",
       "        require([\"brunel\", \"brunelControls\"], function(brunel, brunelControls) {\n",
       "\n",
       "            function  BrunelVis(visId) {\n",
       "  \"use strict\";                                                                       // strict mode\n",
       "  var datasets = [],                                      // array of datasets for the original data\n",
       "      pre = function(d, i) { return d },                         // default pre-process does nothing\n",
       "      post = function(d, i) { return d },                       // default post-process does nothing\n",
       "      transitionTime = 200,                                        // transition time for animations\n",
       "      charts = [],                                                       // the charts in the system\n",
       "      vis = d3.select('#' + visId).attr('class', 'brunel');                     // the SVG container\n",
       "\n",
       "  BrunelD3.addDefinitions(vis);                                   // ensure standard symbols present\n",
       "\n",
       "  // Define chart #1 in the visualization //////////////////////////////////////////////////////////\n",
       "\n",
       "  charts[0] = function(parentNode, filterRows) {\n",
       "    var geom = BrunelD3.geometry(parentNode || vis.node(), 0, 0, 1, 0.5, 22, 43, 96, 0),\n",
       "      elements = [];                                              // array of elements in this chart\n",
       "\n",
       "    // Define groups for the chart parts ///////////////////////////////////////////////////////////\n",
       "\n",
       "    var chart =  vis.append('g').attr('class', 'chart1')\n",
       "      .attr('transform','translate(' + geom.chart_left + ',' + geom.chart_top + ')');\n",
       "    var overlay = chart.append('g').attr('class', 'element').attr('class', 'overlay');\n",
       "    var zoom = d3.zoom().scaleExtent([1/3,3]);\n",
       "    var zoomNode = overlay.append('rect').attr('class', 'overlay')\n",
       "      .attr('x', geom.inner_left).attr('y', geom.inner_top)\n",
       "      .attr('width', geom.inner_rawWidth).attr('height', geom.inner_rawHeight)\n",
       "      .style('cursor', 'move').call(zoom)\n",
       "      .node();\n",
       "    zoomNode.__zoom = d3.zoomIdentity;\n",
       "    chart.append('rect').attr('class', 'background').attr('width', geom.chart_right-geom.chart_left).attr('height', geom.chart_bottom-geom.chart_top);\n",
       "    var interior = chart.append('g').attr('class', 'interior zoomNone')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')')\n",
       "      .attr('clip-path', 'url(#clip_visid3a305647-bed8-4d21-8a22-9fb6811949c8_chart1_inner)');\n",
       "    interior.append('rect').attr('class', 'inner').attr('width', geom.inner_width).attr('height', geom.inner_height);\n",
       "    var gridGroup = interior.append('g').attr('class', 'grid');\n",
       "    var axes = chart.append('g').attr('class', 'axis')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')');\n",
       "    vis.append('clipPath').attr('id', 'clip_visid3a305647-bed8-4d21-8a22-9fb6811949c8_chart1_inner').append('rect')\n",
       "      .attr('x', 0).attr('y', 0)\n",
       "      .attr('width', geom.inner_rawWidth+1).attr('height', geom.inner_rawHeight+1);\n",
       "    chart.append('text').attr('class', 'title header').text('Web Metrics by Feature').style('text-anchor', 'middle')\n",
       "      .attr('x','50%')\n",
       "      .attr('y',2).attr('dy','0.8em');\n",
       "\n",
       "    // Scales //////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    var scale_x = d3.scalePoint().padding(0.5)\n",
       "      .domain(['color', 'camera', 'processor', 'voice_assistant', 'battery'])\n",
       "      .range([0, geom.inner_width]);\n",
       "    var scale_inner = d3.scaleLinear().domain([0,1])\n",
       "      .range([-0.5, 0.5]);\n",
       "    var scale_y = d3.scaleLinear().domain([0, 6.0000006])\n",
       "      .range([geom.inner_height, 0]);\n",
       "    var base_scales = [scale_x, scale_y];                           // untransformed original scales\n",
       "\n",
       "    // Axes ////////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    axes.append('g').attr('class', 'x axis')\n",
       "      .attr('transform','translate(0,' + geom.inner_rawHeight + ')')\n",
       "      .attr('clip-path', 'url(#clip_visid3a305647-bed8-4d21-8a22-9fb6811949c8_chart1_haxis)');\n",
       "    vis.append('clipPath').attr('id', 'clip_visid3a305647-bed8-4d21-8a22-9fb6811949c8_chart1_haxis').append('polyline')\n",
       "      .attr('points', '-1,-1000, -1,-1 -5,5, -1000,5, -100,1000, 10000,1000 10000,-1000');\n",
       "    axes.select('g.axis.x').append('text').attr('class', 'title').text('A-phone features').style('text-anchor', 'middle')\n",
       "      .attr('x',geom.inner_rawWidth/2)\n",
       "      .attr('y', geom.inner_bottom - 2.0).attr('dy','-0.27em');\n",
       "    axes.append('g').attr('class', 'y axis')\n",
       "      .attr('clip-path', 'url(#clip_visid3a305647-bed8-4d21-8a22-9fb6811949c8_chart1_vaxis)');\n",
       "    vis.append('clipPath').attr('id', 'clip_visid3a305647-bed8-4d21-8a22-9fb6811949c8_chart1_vaxis').append('polyline')\n",
       "      .attr('points', '-1000,-10000, 10000,-10000, 10000,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+5) + ', -1000,' + (geom.inner_rawHeight+5) );\n",
       "    axes.select('g.axis.y').append('text').attr('class', 'title').text('page views').style('text-anchor', 'middle')\n",
       "      .attr('x',-geom.inner_rawHeight/2)\n",
       "      .attr('y', 4-geom.inner_left).attr('dy', '0.7em').attr('transform', 'rotate(270)');\n",
       "\n",
       "    var axis_bottom = d3.axisBottom(scale_x).ticks(Math.min(10, Math.round(geom.inner_width / 132.0)));\n",
       "    var axis_left = d3.axisLeft(scale_y).ticks(Math.min(10, Math.round(geom.inner_width / 20)));\n",
       "\n",
       "    function buildAxes(time) {\n",
       "      axis_bottom.tickValues(BrunelD3.filterTicks(scale_x))\n",
       "      var axis_x = axes.select('g.axis.x');\n",
       "      BrunelD3.transition(axis_x, time).call(axis_bottom.scale(scale_x)).selectAll('.tick text')\n",
       "        .attr('transform', function() {\n",
       "          var v = this.getComputedTextLength() / Math.sqrt(2)/2;\n",
       "          return 'translate(-' + (v+6) + ',' + v + ') rotate(-45)'\n",
       "      });\n",
       "      var axis_y = axes.select('g.axis.y');\n",
       "      BrunelD3.transition(axis_y, time).call(axis_left.scale(scale_y));\n",
       "    }\n",
       "    zoom.on('zoom', function(t, time) {\n",
       "        t = t ||BrunelD3.restrictZoom(d3.event.transform, geom, this);\n",
       "        scale_y = t.rescaleY(base_scales[1]);\n",
       "        zoomNode.__zoom = t;\n",
       "        interior.attr('class', 'interior ' + BrunelD3.zoomLabel(t.k));;\n",
       "        build(time || -1);\n",
       "    });\n",
       "\n",
       "    // Define element #1 ///////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    elements[0] = function() {\n",
       "      var original, processed,                           // data sets passed in and then transformed\n",
       "        element, data,                                 // brunel element information and brunel data\n",
       "        selection, merged;                                      // d3 selection and merged selection\n",
       "      var elementGroup = interior.append('g').attr('class', 'element1'),\n",
       "        main = elementGroup.append('g').attr('class', 'main'),\n",
       "        labels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'labels').attr('aria-hidden', 'true'), elementGroup);\n",
       "\n",
       "      function makeData() {\n",
       "        original = datasets[0];\n",
       "        if (filterRows) original = original.retainRows(filterRows);\n",
       "        processed = pre(original, 0)\n",
       "          .sort('page_hits');\n",
       "        processed = post(processed, 0);\n",
       "        var f0 = processed.field('feature'),\n",
       "          f1 = processed.field('page_hits'),\n",
       "          f2 = processed.field('#row'),\n",
       "          f3 = processed.field('#selection');\n",
       "        var keyFunc = function(d) { return f0.value(d) };\n",
       "        data = {\n",
       "          feature:      function(d) { return f0.value(d.row) },\n",
       "          page_hits:    function(d) { return f1.value(d.row) },\n",
       "          $row:         function(d) { return f2.value(d.row) },\n",
       "          $selection:   function(d) { return f3.value(d.row) },\n",
       "          feature_f:    function(d) { return f0.valueFormatted(d.row) },\n",
       "          page_hits_f:  function(d) { return f1.valueFormatted(d.row) },\n",
       "          $row_f:       function(d) { return f2.valueFormatted(d.row) },\n",
       "          $selection_f: function(d) { return f3.valueFormatted(d.row) },\n",
       "          _split:       function(d) { return f0.value(d.row) },\n",
       "          _key:         keyFunc,\n",
       "          _rows:        BrunelD3.makeRowsWithKeys(keyFunc, processed.rowCount())\n",
       "        };\n",
       "      }\n",
       "      // Aesthetic Functions\n",
       "      var scale_color = d3.scaleOrdinal()\n",
       "        .domain(['color', 'camera', 'processor', 'voice_assistant', 'battery'])\n",
       "        .range([ '#347DAD', '#D43F58', '#F7D84A', '#31A461', '#A66A9C', '#FF954D', \n",
       "          '#A7978E', '#FFCA4D', '#F99EAF', '#B1C43B', '#7E64A2', '#FFB04D', '#CA5C7C', \n",
       "          '#DDBC8C', '#FFA28D', '#A5473D', '#8B6141', '#F57357', '#5C6B46']);\n",
       "      var color = function(d) { return scale_color(data.feature(d)) };\n",
       "\n",
       "      // Build element from data ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "      function build(transitionMillis) {\n",
       "        element = elements[0];\n",
       "        var w = 0.9 * Math.abs(scale_x(scale_x.domain()[1]) - scale_x(scale_x.domain()[0]) );\n",
       "        var x = function(d) { return scale_x(data.feature(d))};\n",
       "        var h = Math.abs( scale_y(scale_y.domain()[0] + 1.0) - scale_y.range()[0] );\n",
       "        var y1 = scale_y.range()[0];\n",
       "        var y2 = function(d) { return scale_y(data.page_hits(d))};\n",
       "\n",
       "        // Define selection entry operations\n",
       "        function initialState(selection) {\n",
       "          selection\n",
       "            .attr('class', 'element bar filled')\n",
       "        }\n",
       "\n",
       "        // Define selection update operations on merged data\n",
       "        function updateState(selection) {\n",
       "          selection\n",
       "            .each(function(d) {\n",
       "              var width = w, left = x(d) - width/2, \n",
       "              c = y1, d = y2(d), top = Math.min(c,d), height = Math.max(1e-6, Math.abs(c-d));\n",
       "              this.r = {x:left, y:top, w:width, h:height};\n",
       "            })\n",
       "            .attr('x', function(d) { return this.r.x })\n",
       "            .attr('y', function(d) { return this.r.y })\n",
       "            .attr('width', function(d) { return this.r.w })\n",
       "            .attr('height', function(d) { return this.r.h })\n",
       "            .filter(BrunelD3.hasData)                     // following only performed for data items\n",
       "            .style('fill', color);\n",
       "        }\n",
       "\n",
       "        // Define labeling for the selection\n",
       "        function label(selection, transitionMillis) {\n",
       "\n",
       "          var tooltipLabeling  = {\n",
       "            index: -1, method: 'box', location: ['center', 'top'], inside: true, align: 'middle', pad: 0, dy: 0.7,\n",
       "            fit: true, granularity: 0,\n",
       "            content: function(d) {\n",
       "              return d.row == null ? null : '<span class=\"title\">feature: </span>'\n",
       "\t\t\t+ '<span class=\"field\">' + data.feature_f(d) + '</span>'\n",
       "\t\t\t+ '<br/>'\n",
       "\t\t\t+ '<span class=\"title\">page_hits: </span>'\n",
       "\t\t\t+ '<span class=\"field\">' + data.page_hits_f(d) + '</span>'\n",
       "            }\n",
       "          };\n",
       "          BrunelD3.addTooltip(selection, tooltipLabeling, geom);\n",
       "        }\n",
       "        // Create selections, set the initial state and transition updates\n",
       "        selection = main.selectAll('.element').data(data._rows, function(d) { return d.key });\n",
       "        var added = selection.enter().append('rect');\n",
       "        merged = selection.merge(added);\n",
       "        initialState(added);\n",
       "        selection.filter(BrunelD3.hasData)\n",
       "          .classed('selected', BrunelD3.isSelected(data))\n",
       "          .filter(BrunelD3.isSelected(data)).raise();\n",
       "        updateState(BrunelD3.transition(merged, transitionMillis));\n",
       "        label(merged, transitionMillis);\n",
       "\n",
       "        BrunelD3.transition(selection.exit(), transitionMillis/3)\n",
       "          .style('opacity', 0.5).each( function() {\n",
       "            this.remove(); BrunelD3.removeLabels(this); \n",
       "        });\n",
       "        chart.select('rect.overlay')                               // attach handlers to the overlay\n",
       "          .on('click.interact', function(d) {\n",
       "            var c = BrunelD3.closest(merged, 'xy', geom.inner_radius/4 );\n",
       "            BrunelD3.select(null, this, element, updateAll);\n",
       "          });\n",
       "        merged                                                     // attach handlers to the element\n",
       "          .on('click.interact', function(d) {BrunelD3.select(d, this, element, updateAll) });\n",
       "      }\n",
       "\n",
       "      return {\n",
       "        data:           function() { return processed },\n",
       "        original:       function() { return original },\n",
       "        internal:       function() { return data },\n",
       "        selection:      function() { return merged },\n",
       "        makeData:       makeData,\n",
       "        build:          build,\n",
       "        chart:          function() { return charts[0] },\n",
       "        group:          function() { return elementGroup },\n",
       "        fields: {\n",
       "          x:            ['feature'],\n",
       "          y:            ['page_hits'],\n",
       "          key:          ['feature'],\n",
       "          color:        ['feature']\n",
       "        }\n",
       "      };\n",
       "    }();\n",
       "\n",
       "    function build(time, noData) {\n",
       "      var first = elements[0].data() == null;\n",
       "      if (first) time = 0;                                           // no transition for first call\n",
       "      buildAxes(time);\n",
       "      if ((first || time > -1) && !noData) {\n",
       "        elements[0].makeData();\n",
       "      }\n",
       "      elements[0].build(time);\n",
       "    }\n",
       "\n",
       "    // Expose the following components of the chart\n",
       "    return {\n",
       "      elements : elements,\n",
       "      interior : interior,\n",
       "      scales: {x:scale_x, y:scale_y},\n",
       "      zoom: function(params, time) {\n",
       "          if (params) zoom.on('zoom').call(zoomNode, params, time);\n",
       "          return d3.zoomTransform(zoomNode);\n",
       "      },\n",
       "      build : build\n",
       "    };\n",
       "    }();\n",
       "\n",
       "  // Define chart #2 in the visualization //////////////////////////////////////////////////////////\n",
       "\n",
       "  charts[1] = function(parentNode, filterRows) {\n",
       "    var geom = BrunelD3.geometry(parentNode || vis.node(), 0, 0.5, 1, 1, 0, 0, 0, 0),\n",
       "      elements = [];                                              // array of elements in this chart\n",
       "\n",
       "    // Define groups for the chart parts ///////////////////////////////////////////////////////////\n",
       "\n",
       "    var chart =  vis.append('g').attr('class', 'chart2')\n",
       "      .attr('transform','translate(' + geom.chart_left + ',' + geom.chart_top + ')');\n",
       "    var overlay = chart.append('g').attr('class', 'element').attr('class', 'overlay');\n",
       "    var zoom = d3.zoom().scaleExtent([1/3,3]);\n",
       "    var zoomNode = overlay.append('rect').attr('class', 'overlay')\n",
       "      .attr('x', geom.inner_left).attr('y', geom.inner_top)\n",
       "      .attr('width', geom.inner_rawWidth).attr('height', geom.inner_rawHeight)\n",
       "      .style('cursor', 'default')\n",
       "      .node();\n",
       "    zoomNode.__zoom = d3.zoomIdentity;\n",
       "    chart.append('rect').attr('class', 'background').attr('width', geom.chart_right-geom.chart_left).attr('height', geom.chart_bottom-geom.chart_top);\n",
       "    var interior = chart.append('g').attr('class', 'interior zoomNone')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')')\n",
       "      .attr('clip-path', 'url(#clip_visid3a305647-bed8-4d21-8a22-9fb6811949c8_chart2_inner)');\n",
       "    interior.append('rect').attr('class', 'inner').attr('width', geom.inner_width).attr('height', geom.inner_height);\n",
       "    var gridGroup = interior.append('g').attr('class', 'grid');\n",
       "    vis.append('clipPath').attr('id', 'clip_visid3a305647-bed8-4d21-8a22-9fb6811949c8_chart2_inner').append('rect')\n",
       "      .attr('x', 0).attr('y', 0)\n",
       "      .attr('width', geom.inner_rawWidth+1).attr('height', geom.inner_rawHeight+1);\n",
       "\n",
       "    // Scales //////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    var scale_x = d3.scaleLinear().domain([0,1])\n",
       "      .range([0, geom.inner_radius]);\n",
       "    var scale_inner = d3.scaleLinear().domain([0,1])\n",
       "      .range([-0.5, 0.5]);\n",
       "    var scale_y = d3.scaleLinear().domain([0, 1200.0001])\n",
       "      .range([0, Math.PI*2]);\n",
       "    var base_scales = [scale_x, scale_y];                           // untransformed original scales\n",
       "    zoom.on('zoom', function(t, time) {\n",
       "        t = t ||BrunelD3.restrictZoom(d3.event.transform, geom, this);\n",
       "        zoomNode.__zoom = t;\n",
       "        interior.attr('class', 'interior ' + BrunelD3.zoomLabel(t.k));;\n",
       "        build(time || -1);\n",
       "    });\n",
       "\n",
       "    // Define element #1 ///////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    elements[0] = function() {\n",
       "      var original, processed,                           // data sets passed in and then transformed\n",
       "        element, data,                                 // brunel element information and brunel data\n",
       "        selection, merged;                                      // d3 selection and merged selection\n",
       "      var elementGroup = interior.append('g').attr('class', 'element1')\n",
       "        .attr('transform','translate(' + geom.inner_width/2 + ',' + geom.inner_height/2 + ')'),\n",
       "        main = elementGroup.append('g').attr('class', 'main'),\n",
       "        labels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'labels').attr('aria-hidden', 'true'), elementGroup);\n",
       "\n",
       "      function makeData() {\n",
       "        original = datasets[0];\n",
       "        if (filterRows) original = original.retainRows(filterRows);\n",
       "        processed = pre(original, 0)\n",
       "          .sort('page_hits')\n",
       "          .stack('total_time; ; feature, #selection; false');\n",
       "        processed = post(processed, 0);\n",
       "        var f0 = processed.field('total_time$lower'),\n",
       "          f1 = processed.field('total_time$upper'),\n",
       "          f2 = processed.field('total_time'),\n",
       "          f3 = processed.field('feature'),\n",
       "          f4 = processed.field('#selection'),\n",
       "          f5 = processed.field('page_hits'),\n",
       "          f6 = processed.field('#row');\n",
       "        var keyFunc = function(d) { return f3.value(d) };\n",
       "        data = {\n",
       "          total_time$lower:function(d) { return f0.value(d.row) },\n",
       "          total_time$upper:function(d) { return f1.value(d.row) },\n",
       "          total_time:   function(d) { return f2.value(d.row) },\n",
       "          feature:      function(d) { return f3.value(d.row) },\n",
       "          $selection:   function(d) { return f4.value(d.row) },\n",
       "          page_hits:    function(d) { return f5.value(d.row) },\n",
       "          $row:         function(d) { return f6.value(d.row) },\n",
       "          total_time$lower_f:function(d) { return f0.valueFormatted(d.row) },\n",
       "          total_time$upper_f:function(d) { return f1.valueFormatted(d.row) },\n",
       "          total_time_f: function(d) { return f2.valueFormatted(d.row) },\n",
       "          feature_f:    function(d) { return f3.valueFormatted(d.row) },\n",
       "          $selection_f: function(d) { return f4.valueFormatted(d.row) },\n",
       "          page_hits_f:  function(d) { return f5.valueFormatted(d.row) },\n",
       "          $row_f:       function(d) { return f6.valueFormatted(d.row) },\n",
       "          _split:       function(d) { return f3.value(d.row)+ '|' + f4.value(d.row) },\n",
       "          _key:         keyFunc,\n",
       "          _rows:        BrunelD3.makeRowsWithKeys(keyFunc, processed.rowCount())\n",
       "        };\n",
       "      }\n",
       "      // Aesthetic Functions\n",
       "      var scale_color = d3.scaleOrdinal()\n",
       "        .domain(['color', 'camera', 'processor', 'voice_assistant', 'battery'])\n",
       "        .range([ '#347DAD', '#D43F58', '#F7D84A', '#31A461', '#A66A9C', '#FF954D', \n",
       "          '#A7978E', '#FFCA4D', '#F99EAF', '#B1C43B', '#7E64A2', '#FFB04D', '#CA5C7C', \n",
       "          '#DDBC8C', '#FFA28D', '#A5473D', '#8B6141', '#F57357', '#5C6B46']);\n",
       "      var color = function(d) { return scale_color(data.feature(d)) };\n",
       "      var scale_opacity = d3.scaleOrdinal()\n",
       "        .domain(['✗', '✓'])\n",
       "        .range([0.2, 1.0]);\n",
       "      var opacity = function(d) { return scale_opacity(data.$selection(d)) };\n",
       "\n",
       "      // Build element from data ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "      function build(transitionMillis) {\n",
       "        element = elements[0];\n",
       "        var w = geom.inner_width;\n",
       "        var x = scale_x(0.5);\n",
       "        var h = Math.abs( scale_y(scale_y.domain()[0] + 120.0) - scale_y.range()[0] );\n",
       "        var y1 = function(d) { return scale_y(data.total_time$lower(d))};\n",
       "        var y2 = function(d) { return scale_y(data.total_time$upper(d))};\n",
       "        var y = function(d) { return scale_y( (data.total_time$upper(d) + data.total_time$lower(d) )/2)};\n",
       "        // Define the path for pie wedge shapes\n",
       "        var path = d3.arc().innerRadius(0).outerRadius(geom.inner_radius)\n",
       "          .startAngle(y1).endAngle(y2);\n",
       "        var labeling  = [{\n",
       "          index: 0, method: 'wedge', location: ['center', 'center'], inside: false, align: 'middle', pad: 3, dy: 0.3,\n",
       "          fit: false, granularity: 1,\n",
       "          path: path,\n",
       "          content: function(d) {\n",
       "            return d.row == null ? null : data.feature_f(d)\n",
       "          }\n",
       "        }];\n",
       "\n",
       "        // Define selection entry operations\n",
       "        function initialState(selection) {\n",
       "          selection\n",
       "            .attr('class', 'element bar filled')\n",
       "        }\n",
       "\n",
       "        // Define selection update operations on merged data\n",
       "        function updateState(selection) {\n",
       "          selection\n",
       "            .attr('d', path)\n",
       "            .filter(BrunelD3.hasData)                     // following only performed for data items\n",
       "            .style('fill', color)\n",
       "            .style('fill-opacity', opacity)\n",
       "            .style('stroke-opacity', opacity);\n",
       "        }\n",
       "\n",
       "        // Define labeling for the selection\n",
       "        function label(selection, transitionMillis) {\n",
       "          BrunelD3.label(selection, labels, transitionMillis, geom, labeling);\n",
       "\n",
       "          var tooltipLabeling  = {\n",
       "            index: -1, method: 'wedge', location: ['center', 'center'], inside: true, align: 'middle', pad: 0, dy: 0.3,\n",
       "            fit: true, granularity: 0,\n",
       "            path: path,\n",
       "            content: function(d) {\n",
       "              return d.row == null ? null : 'time on page (sec): '\n",
       "\t\t\t+ '<span class=\"field\">' + data.total_time_f(d) + '</span>'\n",
       "            }\n",
       "          };\n",
       "          BrunelD3.addTooltip(selection, tooltipLabeling, geom);\n",
       "        }\n",
       "        // Create selections, set the initial state and transition updates\n",
       "        selection = main.selectAll('.element').data(data._rows, function(d) { return d.key });\n",
       "        var added = selection.enter().append('path');\n",
       "        merged = selection.merge(added);\n",
       "        initialState(added);\n",
       "        selection.filter(BrunelD3.hasData)\n",
       "          .classed('selected', BrunelD3.isSelected(data))\n",
       "          .filter(BrunelD3.isSelected(data)).raise();\n",
       "        updateState(BrunelD3.transition(merged, transitionMillis));\n",
       "        label(merged, transitionMillis);\n",
       "\n",
       "        BrunelD3.transition(selection.exit(), transitionMillis/3)\n",
       "          .style('opacity', 0.5).each( function() {\n",
       "            this.remove(); BrunelD3.removeLabels(this); \n",
       "        });\n",
       "      }\n",
       "\n",
       "      return {\n",
       "        data:           function() { return processed },\n",
       "        original:       function() { return original },\n",
       "        internal:       function() { return data },\n",
       "        selection:      function() { return merged },\n",
       "        makeData:       makeData,\n",
       "        build:          build,\n",
       "        chart:          function() { return charts[1] },\n",
       "        group:          function() { return elementGroup },\n",
       "        fields: {\n",
       "          y:            ['total_time'],\n",
       "          key:          ['feature'],\n",
       "          color:        ['feature'],\n",
       "          opacity:      ['#selection']\n",
       "        }\n",
       "      };\n",
       "    }();\n",
       "\n",
       "    function build(time, noData) {\n",
       "      var first = elements[0].data() == null;\n",
       "      if (first) time = 0;                                           // no transition for first call\n",
       "      if ((first || time > -1) && !noData) {\n",
       "        elements[0].makeData();\n",
       "      }\n",
       "      elements[0].build(time);\n",
       "    }\n",
       "\n",
       "    // Expose the following components of the chart\n",
       "    return {\n",
       "      elements : elements,\n",
       "      interior : interior,\n",
       "      scales: {x:scale_x, y:scale_y},\n",
       "      zoom: function(params, time) {\n",
       "          if (params) zoom.on('zoom').call(zoomNode, params, time);\n",
       "          return d3.zoomTransform(zoomNode);\n",
       "      },\n",
       "      build : build\n",
       "    };\n",
       "    }();\n",
       "\n",
       "  function setData(rowData, i) { datasets[i||0] = BrunelD3.makeData(rowData) }\n",
       "  function updateAll(time) { charts.forEach(function(x) {x.build(time || 0)}) }\n",
       "  function buildAll() {\n",
       "    for (var i=0;i<arguments.length;i++) setData(arguments[i], i);\n",
       "    updateAll(transitionTime);\n",
       "  }\n",
       "\n",
       "  return {\n",
       "    dataPreProcess:     function(f) { if (f) pre = f; return pre },\n",
       "    dataPostProcess:    function(f) { if (f) post = f; return post },\n",
       "    data:               function(d,i) { if (d) setData(d,i); return datasets[i||0] },\n",
       "    visId:              visId,\n",
       "    build:              buildAll,\n",
       "    rebuild:            updateAll,\n",
       "    charts:             charts\n",
       "  }\n",
       "}\n",
       "\n",
       "// Data Tables /////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "var table1 = {\n",
       "   summarized: false,\n",
       "   names: ['feature', 'page_hits', 'total_time'], \n",
       "   options: ['string', 'numeric', 'numeric'], \n",
       "   rows: [['color', 5, 660], ['battery', 1, 120], ['processor', 2, 120], ['voice_assistant', 2, 120],\n",
       "  ['camera', 3, 180]]\n",
       "};\n",
       "\n",
       "// Call Code to Build the system ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "var v = new BrunelVis('visid3a305647-bed8-4d21-8a22-9fb6811949c8');\n",
       "v .build(table1);\n",
       "\n",
       "            \"\"\n",
       "        });\n",
       "        });\n",
       "        </script>"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "%%brunel data('featureMetrics') \n",
    "bar title(\"Web Metrics by Feature\")\n",
    "  x(feature) y(page_hits)\n",
    "  tooltip(feature,page_hits) color(feature) legends(none)\n",
    "  axes(x:'A-phone features',y:'page views') sort(page_hits) interaction(select)|\n",
    "stack polar bar\n",
    "  y(total_time) color(feature) label(feature)\n",
    "  tooltip(\"time on page (sec): \",total_time)\n",
    "  legends(none) sort(page_hits) opacity(#selection)\n",
    ":: width=1000, height=300"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Detailed web metrics for user 'David'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+--------------------+----+-----+----+---+---------+---------+----------+\n",
      "|             pageURL|year|month|week|day|dayofweek|page_hits|total_time|\n",
      "+--------------------+----+-----+----+---+---------+---------+----------+\n",
      "|  /www.cybershop.com|2017|    6|  24| 16|      Fri|        1|        60|\n",
      "|/estore?product_l...|2017|    6|  24| 16|      Fri|        1|       240|\n",
      "|/estore?product_l...|2017|    6|  24| 16|      Fri|        1|       240|\n",
      "|/estore?product_l...|2017|    6|  24| 16|      Fri|        1|       180|\n",
      "|/estore?product_l...|2017|    6|  24| 16|      Fri|        1|       120|\n",
      "+--------------------+----+-----+----+---+---------+---------+----------+\n",
      "only showing top 5 rows\n",
      "\n"
     ]
    }
   ],
   "source": [
    "val userClicksQuery =\"\"\"\n",
    "  select pageURL, year(date) as year, month(date) as month, weekofyear(date) as week,\n",
    "    day(date) as day, date_format(date, 'E') as dayofweek,\n",
    "    count(*) as page_hits, sum(time) as total_time\n",
    "  from ClickData\n",
    "  where eventType='pageView' and userId='datkins' group by pageURL, date\"\"\"\n",
    "val userClicksDF = sqlContext.sql(userClicksQuery)        \n",
    "userClicksDF.show(5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+-----+----+---+---------+------------+-------+-------+---------------+---------+----------+\n",
      "|month|week|day|dayofweek|product_line| action|product|        feature|page_hits|total_time|\n",
      "+-----+----+---+---------+------------+-------+-------+---------------+---------+----------+\n",
      "|    6|  24| 16|      Fri|  headphones|details| A-head|               |        1|       240|\n",
      "|    6|  24| 16|      Fri|  headphones|details| B-head|               |        1|       240|\n",
      "|    6|  24| 16|      Fri| smartphones|details|M-phone|               |        1|       180|\n",
      "|    6|  24| 16|      Fri| smartphones|details|S-phone|               |        1|       120|\n",
      "|    6|  24| 16|      Fri| smartphones|details|S-phone|voice_assistant|        1|        60|\n",
      "+-----+----+---+---------+------------+-------+-------+---------------+---------+----------+\n",
      "only showing top 5 rows\n",
      "\n"
     ]
    }
   ],
   "source": [
    "// Build user web metrics by product_line, products and feature browses\n",
    "userClicksDF.registerTempTable(\"UserWebMetricsData\")\n",
    "val metricsQuery = \"\"\"\n",
    "  select month, week, day, dayofweek,\n",
    "    parse_URL(pageURL,'QUERY','product_line') as product_line, \n",
    "    Coalesce(parse_URL(pageURL,'QUERY','action'),'') as action,\n",
    "    Coalesce(parse_URL(pageURL,'QUERY','product'),'') as product, \n",
    "    Coalesce(parse_URL(pageURL,'QUERY','feature'),'') as feature,\n",
    "    page_hits, total_time\n",
    "  from UserWebMetricsData\n",
    "  where year = '2017'\"\"\"\n",
    "\n",
    "val userWebMetricsDF = sqlContext.sql(metricsQuery).filter($\"product_line\".isNotNull)\n",
    "userWebMetricsDF.show(5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+---------+---+------------+-------+-------+---------------+---------+----------+---------+\n",
      "|dayofweek|day|product_line| action|product|        feature|page_hits|total_time|max(week)|\n",
      "+---------+---+------------+-------+-------+---------------+---------+----------+---------+\n",
      "|      Fri| 16|  headphones|details| A-head|               |        1|       240|       24|\n",
      "|      Fri| 16|  headphones|details| B-head|               |        1|       240|       24|\n",
      "|      Fri| 16| smartphones|details|M-phone|               |        1|       180|       24|\n",
      "|      Fri| 16| smartphones|details|S-phone|               |        1|       120|       24|\n",
      "|      Fri| 16| smartphones|details|S-phone|voice_assistant|        1|        60|       24|\n",
      "+---------+---+------------+-------+-------+---------------+---------+----------+---------+\n",
      "only showing top 5 rows\n",
      "\n"
     ]
    }
   ],
   "source": [
    "// Metrics for most recent week\n",
    "val weekMetricsDF = userWebMetricsDF.\n",
    "  groupBy(\"dayofweek\", \"day\", \"product_line\", \"action\", \"product\", \"feature\", \"page_hits\", \"total_time\").\n",
    "  max(\"week\")\n",
    "weekMetricsDF.show(5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "         <link rel=\"stylesheet\" type=\"text/css\" href=\"https://brunelvis.org/js/brunel.2.3.css\" charset=\"utf-8\">\n",
       "         <link rel=\"stylesheet\" type=\"text/css\" href=\"https://brunelvis.org/js/sumoselect.css\" charset=\"utf-8\">\n",
       "         <style>  </style>\n",
       "         <div id=\"controlsIdc50e0c14-1b2e-418a-a659-abdb4342028a\" class=\"brunel\"/>\n",
       "<svg id=\"visid6a894250-69c7-4e00-b12d-dbf9d3f0c2e8\" width=\"1000\" height=\"300\"></svg>\n",
       "\n",
       "<script>\n",
       "require.config({\n",
       "            waitSeconds: 60,\n",
       "            paths: {\n",
       "                'd3': '//cdnjs.cloudflare.com/ajax/libs/d3/4.2.1/d3.min',\n",
       "                'topojson' : '//cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min',\n",
       "                'brunel' : 'https://brunelvis.org/js/brunel.2.3.min',\n",
       "                'brunelControls' : 'https://brunelvis.org/js/brunel.controls.2.3.min'\n",
       "            },\n",
       "\n",
       "            shim: {\n",
       "               'brunel' : {\n",
       "                    exports: 'BrunelD3',\n",
       "                    deps: ['d3', 'topojson'],\n",
       "                    init: function() {\n",
       "                       return {\n",
       "                         BrunelD3 : BrunelD3,\n",
       "                         BrunelData : BrunelData\n",
       "                      }\n",
       "                    }\n",
       "                },\n",
       "               'brunelControls' : {\n",
       "                    exports: 'BrunelEventHandlers',\n",
       "                    init: function() {\n",
       "                       return {\n",
       "                         BrunelEventHandlers: BrunelEventHandlers,\n",
       "                         BrunelJQueryControlFactory: BrunelJQueryControlFactory\n",
       "                      }\n",
       "                    }\n",
       "                }\n",
       "\n",
       "            }\n",
       "\n",
       "        });\n",
       "\n",
       "        require([\"d3\"], function(d3) {\n",
       "        require([\"brunel\", \"brunelControls\"], function(brunel, brunelControls) {\n",
       "\n",
       "            function  BrunelVis(visId) {\n",
       "  \"use strict\";                                                                       // strict mode\n",
       "  var datasets = [],                                      // array of datasets for the original data\n",
       "      pre = function(d, i) { return d },                         // default pre-process does nothing\n",
       "      post = function(d, i) { return d },                       // default post-process does nothing\n",
       "      transitionTime = 200,                                        // transition time for animations\n",
       "      charts = [],                                                       // the charts in the system\n",
       "      vis = d3.select('#' + visId).attr('class', 'brunel');                     // the SVG container\n",
       "\n",
       "  BrunelD3.addDefinitions(vis);                                   // ensure standard symbols present\n",
       "\n",
       "  // Define chart #1 in the visualization //////////////////////////////////////////////////////////\n",
       "\n",
       "  charts[0] = function(parentNode, filterRows) {\n",
       "    var geom = BrunelD3.geometry(parentNode || vis.node(), 0, 0, 1, 0.5, 22, 43, 37, 13),\n",
       "      elements = [];                                              // array of elements in this chart\n",
       "\n",
       "    // Define groups for the chart parts ///////////////////////////////////////////////////////////\n",
       "\n",
       "    var chart =  vis.append('g').attr('class', 'chart1')\n",
       "      .attr('transform','translate(' + geom.chart_left + ',' + geom.chart_top + ')');\n",
       "    var overlay = chart.append('g').attr('class', 'element').attr('class', 'overlay');\n",
       "    var zoom = d3.zoom().scaleExtent([1/3,3]);\n",
       "    var zoomNode = overlay.append('rect').attr('class', 'overlay')\n",
       "      .attr('x', geom.inner_left).attr('y', geom.inner_top)\n",
       "      .attr('width', geom.inner_rawWidth).attr('height', geom.inner_rawHeight)\n",
       "      .style('cursor', 'move').call(zoom)\n",
       "      .node();\n",
       "    zoomNode.__zoom = d3.zoomIdentity;\n",
       "    chart.append('rect').attr('class', 'background').attr('width', geom.chart_right-geom.chart_left).attr('height', geom.chart_bottom-geom.chart_top);\n",
       "    var interior = chart.append('g').attr('class', 'interior zoomNone')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')')\n",
       "      .attr('clip-path', 'url(#clip_visid6a894250-69c7-4e00-b12d-dbf9d3f0c2e8_chart1_inner)');\n",
       "    interior.append('rect').attr('class', 'inner').attr('width', geom.inner_width).attr('height', geom.inner_height);\n",
       "    var gridGroup = interior.append('g').attr('class', 'grid');\n",
       "    var axes = chart.append('g').attr('class', 'axis')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')');\n",
       "    vis.append('clipPath').attr('id', 'clip_visid6a894250-69c7-4e00-b12d-dbf9d3f0c2e8_chart1_inner').append('rect')\n",
       "      .attr('x', 0).attr('y', 0)\n",
       "      .attr('width', geom.inner_rawWidth+1).attr('height', geom.inner_rawHeight+1);\n",
       "    chart.append('text').attr('class', 'title header').text(\"David's Browsing by Day\").style('text-anchor', 'middle')\n",
       "      .attr('x','50%')\n",
       "      .attr('y',2).attr('dy','0.8em');\n",
       "\n",
       "    // Scales //////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    var scale_x = d3.scaleLinear().domain([15, 22.000001])\n",
       "      .range([0, geom.inner_width]);\n",
       "    var scale_inner = d3.scaleLinear().domain([0,1])\n",
       "      .range([-0.5, 0.5]);\n",
       "    var scale_y = d3.scaleSqrt().domain([0, 8.0000008])\n",
       "      .range([geom.inner_height, 0]);\n",
       "    var base_scales = [scale_x, scale_y];                           // untransformed original scales\n",
       "\n",
       "    // Axes ////////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    axes.append('g').attr('class', 'x axis')\n",
       "      .attr('transform','translate(0,' + geom.inner_rawHeight + ')')\n",
       "      .attr('clip-path', 'url(#clip_visid6a894250-69c7-4e00-b12d-dbf9d3f0c2e8_chart1_haxis)');\n",
       "    vis.append('clipPath').attr('id', 'clip_visid6a894250-69c7-4e00-b12d-dbf9d3f0c2e8_chart1_haxis').append('polyline')\n",
       "      .attr('points', '-1,-1000, -1,-1 -5,5, -1000,5, -100,1000, 10000,1000 10000,-1000');\n",
       "    axes.select('g.axis.x').append('text').attr('class', 'title').text('day').style('text-anchor', 'middle')\n",
       "      .attr('x',geom.inner_rawWidth/2)\n",
       "      .attr('y', geom.inner_bottom - 2.0).attr('dy','-0.27em');\n",
       "    axes.append('g').attr('class', 'y axis')\n",
       "      .attr('clip-path', 'url(#clip_visid6a894250-69c7-4e00-b12d-dbf9d3f0c2e8_chart1_vaxis)');\n",
       "    vis.append('clipPath').attr('id', 'clip_visid6a894250-69c7-4e00-b12d-dbf9d3f0c2e8_chart1_vaxis').append('polyline')\n",
       "      .attr('points', '-1000,-10000, 10000,-10000, 10000,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+5) + ', -1000,' + (geom.inner_rawHeight+5) );\n",
       "    axes.select('g.axis.y').append('text').attr('class', 'title').text('page views').style('text-anchor', 'middle')\n",
       "      .attr('x',-geom.inner_rawHeight/2)\n",
       "      .attr('y', 4-geom.inner_left).attr('dy', '0.7em').attr('transform', 'rotate(270)');\n",
       "\n",
       "    var axis_bottom = d3.axisBottom(scale_x).ticks(7);\n",
       "    var axis_left = d3.axisLeft(scale_y).ticks(Math.min(10, Math.round(geom.inner_width / 20)));\n",
       "\n",
       "    function buildAxes(time) {\n",
       "      var axis_x = axes.select('g.axis.x');\n",
       "      BrunelD3.transition(axis_x, time).call(axis_bottom.scale(scale_x));\n",
       "      var axis_y = axes.select('g.axis.y');\n",
       "      BrunelD3.transition(axis_y, time).call(axis_left.scale(scale_y));\n",
       "    }\n",
       "    zoom.on('zoom', function(t, time) {\n",
       "        t = t ||BrunelD3.restrictZoom(d3.event.transform, geom, this);\n",
       "        scale_x = t.rescaleX(base_scales[0]);\n",
       "        scale_y = t.rescaleY(base_scales[1]);\n",
       "        zoomNode.__zoom = t;\n",
       "        interior.attr('class', 'interior ' + BrunelD3.zoomLabel(t.k));;\n",
       "        build(time || -1);\n",
       "    });\n",
       "\n",
       "    // Define element #1 ///////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    elements[0] = function() {\n",
       "      var original, processed,                           // data sets passed in and then transformed\n",
       "        element, data,                                 // brunel element information and brunel data\n",
       "        selection, merged;                                      // d3 selection and merged selection\n",
       "      var elementGroup = interior.append('g').attr('class', 'element1'),\n",
       "        main = elementGroup.append('g').attr('class', 'main'),\n",
       "        labels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'labels').attr('aria-hidden', 'true'), elementGroup);\n",
       "\n",
       "      function makeData() {\n",
       "        original = datasets[0];\n",
       "        if (filterRows) original = original.retainRows(filterRows);\n",
       "        processed = pre(original, 0)\n",
       "          .summarize('page_hits=page_hits:sum; day=day:base; product_line=product_line')\n",
       "          .stack('page_hits; day; product_line; false');\n",
       "        processed = post(processed, 0);\n",
       "        var f0 = processed.field('page_hits$lower'),\n",
       "          f1 = processed.field('page_hits$upper'),\n",
       "          f2 = processed.field('day'),\n",
       "          f3 = processed.field('page_hits'),\n",
       "          f4 = processed.field('product_line'),\n",
       "          f5 = processed.field('#row'),\n",
       "          f6 = processed.field('#selection');\n",
       "        var keyFunc = function(d) { return f5.value(d) };\n",
       "        data = {\n",
       "          page_hits$lower:function(d) { return f0.value(d.row) },\n",
       "          page_hits$upper:function(d) { return f1.value(d.row) },\n",
       "          day:          function(d) { return f2.value(d.row) },\n",
       "          page_hits:    function(d) { return f3.value(d.row) },\n",
       "          product_line: function(d) { return f4.value(d.row) },\n",
       "          $row:         function(d) { return f5.value(d.row) },\n",
       "          $selection:   function(d) { return f6.value(d.row) },\n",
       "          page_hits$lower_f:function(d) { return f0.valueFormatted(d.row) },\n",
       "          page_hits$upper_f:function(d) { return f1.valueFormatted(d.row) },\n",
       "          day_f:        function(d) { return f2.valueFormatted(d.row) },\n",
       "          page_hits_f:  function(d) { return f3.valueFormatted(d.row) },\n",
       "          product_line_f:function(d) { return f4.valueFormatted(d.row) },\n",
       "          $row_f:       function(d) { return f5.valueFormatted(d.row) },\n",
       "          $selection_f: function(d) { return f6.valueFormatted(d.row) },\n",
       "          _split:       function(d) { return f4.value(d.row) },\n",
       "          _key:         keyFunc,\n",
       "          _rows:        BrunelD3.makeRowsWithKeys(keyFunc, processed.rowCount())\n",
       "        };\n",
       "      }\n",
       "      // Aesthetic Functions\n",
       "      var scale_color = d3.scaleOrdinal()\n",
       "        .domain(['computers', 'headphones', 'smartphones', 'videogames'])\n",
       "        .range([ '#347DAD', '#D43F58', '#F7D84A', '#31A461', '#A66A9C', '#FF954D', \n",
       "          '#A7978E', '#FFCA4D', '#F99EAF', '#B1C43B', '#7E64A2', '#FFB04D', '#CA5C7C', \n",
       "          '#DDBC8C', '#FFA28D', '#A5473D', '#8B6141', '#F57357', '#5C6B46']);\n",
       "      var color = function(d) { return scale_color(data.product_line(d)) };\n",
       "\n",
       "      // Build element from data ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "      function build(transitionMillis) {\n",
       "        element = elements[0];\n",
       "        var w = Math.abs( scale_x(scale_x.domain()[0] + 1.0) - scale_x.range()[0] );\n",
       "        var x = function(d) { return scale_x(data.day(d))};\n",
       "        var h = Math.abs( scale_y(scale_y.domain()[0] + 1.0) - scale_y.range()[0] );\n",
       "        var y1 = function(d) { return scale_y(data.page_hits$lower(d))};\n",
       "        var y2 = function(d) { return scale_y(data.page_hits$upper(d))};\n",
       "        var y = function(d) { return scale_y( (data.page_hits$upper(d) + data.page_hits$lower(d) )/2)};\n",
       "\n",
       "        // Define selection entry operations\n",
       "        function initialState(selection) {\n",
       "          selection\n",
       "            .attr('class', 'element bar filled')\n",
       "        }\n",
       "\n",
       "        // Define selection update operations on merged data\n",
       "        function updateState(selection) {\n",
       "          selection\n",
       "            .each(function(d) {\n",
       "              var width = w, left = x(d) - width/2, \n",
       "              c = y1(d), d = y2(d), top = Math.min(c,d), height = Math.max(1e-6, Math.abs(c-d));\n",
       "              this.r = {x:left, y:top, w:width, h:height};\n",
       "            })\n",
       "            .attr('x', function(d) { return this.r.x })\n",
       "            .attr('y', function(d) { return this.r.y })\n",
       "            .attr('width', function(d) { return this.r.w })\n",
       "            .attr('height', function(d) { return this.r.h })\n",
       "            .filter(BrunelD3.hasData)                     // following only performed for data items\n",
       "            .style('fill', color);\n",
       "        }\n",
       "\n",
       "        // Define labeling for the selection\n",
       "        function label(selection, transitionMillis) {\n",
       "\n",
       "          var tooltipLabeling  = {\n",
       "            index: -1, method: 'box', location: ['center', 'top'], inside: true, align: 'middle', pad: 0, dy: 0.7,\n",
       "            fit: true, granularity: 0,\n",
       "            content: function(d) {\n",
       "              return d.row == null ? null : '<span class=\"title\">day: </span>'\n",
       "\t\t\t+ '<span class=\"field\">' + data.day_f(d) + '</span>'\n",
       "\t\t\t+ '<br/>'\n",
       "\t\t\t+ '<span class=\"title\">Sum(page_hits): </span>'\n",
       "\t\t\t+ '<span class=\"field\">' + data.page_hits_f(d) + '</span>'\n",
       "\t\t\t+ '<br/>'\n",
       "\t\t\t+ '<span class=\"title\">product_line: </span>'\n",
       "\t\t\t+ '<span class=\"field\">' + data.product_line_f(d) + '</span>'\n",
       "            }\n",
       "          };\n",
       "          BrunelD3.addTooltip(selection, tooltipLabeling, geom);\n",
       "        }\n",
       "        // Create selections, set the initial state and transition updates\n",
       "        selection = main.selectAll('.element').data(data._rows, function(d) { return d.key });\n",
       "        var added = selection.enter().append('rect');\n",
       "        merged = selection.merge(added);\n",
       "        initialState(added);\n",
       "        selection.filter(BrunelD3.hasData)\n",
       "          .classed('selected', BrunelD3.isSelected(data))\n",
       "          .filter(BrunelD3.isSelected(data)).raise();\n",
       "        updateState(BrunelD3.transition(merged, transitionMillis));\n",
       "        label(merged, transitionMillis);\n",
       "\n",
       "        BrunelD3.transition(selection.exit(), transitionMillis/3)\n",
       "          .style('opacity', 0.5).each( function() {\n",
       "            this.remove(); BrunelD3.removeLabels(this); \n",
       "        });\n",
       "        chart.select('rect.overlay')                               // attach handlers to the overlay\n",
       "          .on('click.interact', function(d) {\n",
       "            var c = BrunelD3.closest(merged, 'xy', geom.inner_radius/4 );\n",
       "            BrunelD3.select(null, this, element, updateAll);\n",
       "          });\n",
       "        merged                                                     // attach handlers to the element\n",
       "          .on('click.interact', function(d) {BrunelD3.select(d, this, element, updateAll) });\n",
       "      }\n",
       "\n",
       "      return {\n",
       "        data:           function() { return processed },\n",
       "        original:       function() { return original },\n",
       "        internal:       function() { return data },\n",
       "        selection:      function() { return merged },\n",
       "        makeData:       makeData,\n",
       "        build:          build,\n",
       "        chart:          function() { return charts[0] },\n",
       "        group:          function() { return elementGroup },\n",
       "        fields: {\n",
       "          x:            ['day'],\n",
       "          y:            ['page_hits'],\n",
       "          key:          ['#row'],\n",
       "          color:        ['product_line']\n",
       "        }\n",
       "      };\n",
       "    }();\n",
       "\n",
       "    function build(time, noData) {\n",
       "      var first = elements[0].data() == null;\n",
       "      if (first) time = 0;                                           // no transition for first call\n",
       "      buildAxes(time);\n",
       "      if ((first || time > -1) && !noData) {\n",
       "        elements[0].makeData();\n",
       "      }\n",
       "      elements[0].build(time);\n",
       "    }\n",
       "\n",
       "    // Expose the following components of the chart\n",
       "    return {\n",
       "      elements : elements,\n",
       "      interior : interior,\n",
       "      scales: {x:scale_x, y:scale_y},\n",
       "      zoom: function(params, time) {\n",
       "          if (params) zoom.on('zoom').call(zoomNode, params, time);\n",
       "          return d3.zoomTransform(zoomNode);\n",
       "      },\n",
       "      build : build\n",
       "    };\n",
       "    }();\n",
       "\n",
       "  // Define chart #2 in the visualization //////////////////////////////////////////////////////////\n",
       "\n",
       "  charts[1] = function(parentNode, filterRows) {\n",
       "    var geom = BrunelD3.geometry(parentNode || vis.node(), 0, 0.5, 1, 1, 0, 0, 0, 104),\n",
       "      elements = [];                                              // array of elements in this chart\n",
       "\n",
       "    // Define groups for the chart parts ///////////////////////////////////////////////////////////\n",
       "\n",
       "    var chart =  vis.append('g').attr('class', 'chart2')\n",
       "      .attr('transform','translate(' + geom.chart_left + ',' + geom.chart_top + ')');\n",
       "    var overlay = chart.append('g').attr('class', 'element').attr('class', 'overlay');\n",
       "    var zoom = d3.zoom().scaleExtent([1/3,3]);\n",
       "    var zoomNode = overlay.append('rect').attr('class', 'overlay')\n",
       "      .attr('x', geom.inner_left).attr('y', geom.inner_top)\n",
       "      .attr('width', geom.inner_rawWidth).attr('height', geom.inner_rawHeight)\n",
       "      .style('cursor', 'default')\n",
       "      .node();\n",
       "    zoomNode.__zoom = d3.zoomIdentity;\n",
       "    chart.append('rect').attr('class', 'background').attr('width', geom.chart_right-geom.chart_left).attr('height', geom.chart_bottom-geom.chart_top);\n",
       "    var interior = chart.append('g').attr('class', 'interior zoomNone')\n",
       "      .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')')\n",
       "      .attr('clip-path', 'url(#clip_visid6a894250-69c7-4e00-b12d-dbf9d3f0c2e8_chart2_inner)');\n",
       "    interior.append('rect').attr('class', 'inner').attr('width', geom.inner_width).attr('height', geom.inner_height);\n",
       "    var gridGroup = interior.append('g').attr('class', 'grid');\n",
       "    var legends = chart.append('g').attr('class', 'legend')\n",
       "      .attr('transform','translate(' + (geom.chart_right-geom.chart_left - 3) + ',' + 0 + ')');\n",
       "    vis.append('clipPath').attr('id', 'clip_visid6a894250-69c7-4e00-b12d-dbf9d3f0c2e8_chart2_inner').append('rect')\n",
       "      .attr('x', 0).attr('y', 0)\n",
       "      .attr('width', geom.inner_rawWidth+1).attr('height', geom.inner_rawHeight+1);\n",
       "\n",
       "    // Scales //////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    var scale_x = d3.scaleLinear().domain([0,1])\n",
       "      .range([0, geom.inner_radius]);\n",
       "    var scale_inner = d3.scaleLinear().domain([0,1])\n",
       "      .range([-0.5, 0.5]);\n",
       "    var scale_y = d3.scaleLinear().domain([0, 2520.0003])\n",
       "      .range([0, Math.PI*2]);\n",
       "    var base_scales = [scale_x, scale_y];                           // untransformed original scales\n",
       "    zoom.on('zoom', function(t, time) {\n",
       "        t = t ||BrunelD3.restrictZoom(d3.event.transform, geom, this);\n",
       "        zoomNode.__zoom = t;\n",
       "        interior.attr('class', 'interior ' + BrunelD3.zoomLabel(t.k));;\n",
       "        build(time || -1);\n",
       "    });\n",
       "\n",
       "    // Define element #1 ///////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "    elements[0] = function() {\n",
       "      var original, processed,                           // data sets passed in and then transformed\n",
       "        element, data,                                 // brunel element information and brunel data\n",
       "        selection, merged;                                      // d3 selection and merged selection\n",
       "      var elementGroup = interior.append('g').attr('class', 'element1')\n",
       "        .attr('transform','translate(' + geom.inner_width/2 + ',' + geom.inner_height/2 + ')'),\n",
       "        main = elementGroup.append('g').attr('class', 'main'),\n",
       "        labels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'labels').attr('aria-hidden', 'true'), elementGroup);\n",
       "\n",
       "      function makeData() {\n",
       "        original = datasets[0];\n",
       "        if (filterRows) original = original.retainRows(filterRows);\n",
       "        processed = pre(original, 0)\n",
       "          .stack('total_time; ; product_line, #selection; false');\n",
       "        processed = post(processed, 0);\n",
       "        var f0 = processed.field('total_time$lower'),\n",
       "          f1 = processed.field('total_time$upper'),\n",
       "          f2 = processed.field('total_time'),\n",
       "          f3 = processed.field('product_line'),\n",
       "          f4 = processed.field('#selection'),\n",
       "          f5 = processed.field('product'),\n",
       "          f6 = processed.field('dayofweek'),\n",
       "          f7 = processed.field('day'),\n",
       "          f8 = processed.field('#row');\n",
       "        var keyFunc = function(d) { return f8.value(d) };\n",
       "        data = {\n",
       "          total_time$lower:function(d) { return f0.value(d.row) },\n",
       "          total_time$upper:function(d) { return f1.value(d.row) },\n",
       "          total_time:   function(d) { return f2.value(d.row) },\n",
       "          product_line: function(d) { return f3.value(d.row) },\n",
       "          $selection:   function(d) { return f4.value(d.row) },\n",
       "          product:      function(d) { return f5.value(d.row) },\n",
       "          dayofweek:    function(d) { return f6.value(d.row) },\n",
       "          day:          function(d) { return f7.value(d.row) },\n",
       "          $row:         function(d) { return f8.value(d.row) },\n",
       "          total_time$lower_f:function(d) { return f0.valueFormatted(d.row) },\n",
       "          total_time$upper_f:function(d) { return f1.valueFormatted(d.row) },\n",
       "          total_time_f: function(d) { return f2.valueFormatted(d.row) },\n",
       "          product_line_f:function(d) { return f3.valueFormatted(d.row) },\n",
       "          $selection_f: function(d) { return f4.valueFormatted(d.row) },\n",
       "          product_f:    function(d) { return f5.valueFormatted(d.row) },\n",
       "          dayofweek_f:  function(d) { return f6.valueFormatted(d.row) },\n",
       "          day_f:        function(d) { return f7.valueFormatted(d.row) },\n",
       "          $row_f:       function(d) { return f8.valueFormatted(d.row) },\n",
       "          _split:       function(d) { return f3.value(d.row)+ '|' + f4.value(d.row) },\n",
       "          _key:         keyFunc,\n",
       "          _rows:        BrunelD3.makeRowsWithKeys(keyFunc, processed.rowCount())\n",
       "        };\n",
       "      }\n",
       "      // Aesthetic Functions\n",
       "      var scale_color = d3.scaleOrdinal()\n",
       "        .domain(['computers', 'headphones', 'smartphones', 'videogames'])\n",
       "        .range([ '#347DAD', '#D43F58', '#F7D84A', '#31A461', '#A66A9C', '#FF954D', \n",
       "          '#A7978E', '#FFCA4D', '#F99EAF', '#B1C43B', '#7E64A2', '#FFB04D', '#CA5C7C', \n",
       "          '#DDBC8C', '#FFA28D', '#A5473D', '#8B6141', '#F57357', '#5C6B46']);\n",
       "      var color = function(d) { return scale_color(data.product_line(d)) };\n",
       "      var scale_opacity = d3.scaleOrdinal()\n",
       "        .domain(['✗', '✓'])\n",
       "        .range([0.2, 1.0]);\n",
       "      var opacity = function(d) { return scale_opacity(data.$selection(d)) };\n",
       "      legends._legend = legends._legend || { title: ['product_line'], \n",
       "        ticks: scale_color.domain()};\n",
       "      legends._legend.color = scale_color;\n",
       "\n",
       "      // Build element from data ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "      function build(transitionMillis) {\n",
       "        element = elements[0];\n",
       "        var w = geom.inner_width;\n",
       "        var x = scale_x(0.5);\n",
       "        var h = Math.abs( scale_y(scale_y.domain()[0] + 60.0) - scale_y.range()[0] );\n",
       "        var y1 = function(d) { return scale_y(data.total_time$lower(d))};\n",
       "        var y2 = function(d) { return scale_y(data.total_time$upper(d))};\n",
       "        var y = function(d) { return scale_y( (data.total_time$upper(d) + data.total_time$lower(d) )/2)};\n",
       "        // Define the path for pie wedge shapes\n",
       "        var path = d3.arc().innerRadius(0).outerRadius(geom.inner_radius)\n",
       "          .startAngle(y1).endAngle(y2);\n",
       "        var labeling  = [{\n",
       "          index: 0, method: 'wedge', location: ['center', 'center'], inside: false, align: 'middle', pad: 3, dy: 0.3,\n",
       "          fit: false, granularity: 1,\n",
       "          path: path,\n",
       "          content: function(d) {\n",
       "            return d.row == null ? null : data.product_f(d)\n",
       "          }\n",
       "        }];\n",
       "\n",
       "        // Define selection entry operations\n",
       "        function initialState(selection) {\n",
       "          selection\n",
       "            .attr('class', 'element bar filled')\n",
       "        }\n",
       "\n",
       "        // Define selection update operations on merged data\n",
       "        function updateState(selection) {\n",
       "          selection\n",
       "            .attr('d', path)\n",
       "            .filter(BrunelD3.hasData)                     // following only performed for data items\n",
       "            .style('fill', color)\n",
       "            .style('fill-opacity', opacity)\n",
       "            .style('stroke-opacity', opacity);\n",
       "        }\n",
       "\n",
       "        // Define labeling for the selection\n",
       "        function label(selection, transitionMillis) {\n",
       "          BrunelD3.label(selection, labels, transitionMillis, geom, labeling);\n",
       "\n",
       "          var tooltipLabeling  = {\n",
       "            index: -1, method: 'wedge', location: ['center', 'center'], inside: true, align: 'middle', pad: 0, dy: 0.3,\n",
       "            fit: true, granularity: 0,\n",
       "            path: path,\n",
       "            content: function(d) {\n",
       "              return d.row == null ? null : '<b>day of week: '\n",
       "\t\t\t+ '<span class=\"field\">' + data.dayofweek_f(d) + '</span>'\n",
       "\t\t\t+ '<p><u>day of month: '\n",
       "\t\t\t+ '<span class=\"field\">' + data.day_f(d) + '</span>'\n",
       "\t\t\t+ '</u></b><p><i>product line: '\n",
       "\t\t\t+ '<span class=\"field\">' + data.product_line_f(d) + '</span>'\n",
       "\t\t\t+ '</i><p>product: '\n",
       "\t\t\t+ '<span class=\"field\">' + data.product_f(d) + '</span>'\n",
       "            }\n",
       "          };\n",
       "          BrunelD3.addTooltip(selection, tooltipLabeling, geom);\n",
       "        }\n",
       "        // Create selections, set the initial state and transition updates\n",
       "        selection = main.selectAll('.element').data(data._rows, function(d) { return d.key });\n",
       "        var added = selection.enter().append('path');\n",
       "        merged = selection.merge(added);\n",
       "        initialState(added);\n",
       "        selection.filter(BrunelD3.hasData)\n",
       "          .classed('selected', BrunelD3.isSelected(data))\n",
       "          .filter(BrunelD3.isSelected(data)).raise();\n",
       "        updateState(BrunelD3.transition(merged, transitionMillis));\n",
       "        label(merged, transitionMillis);\n",
       "\n",
       "        BrunelD3.transition(selection.exit(), transitionMillis/3)\n",
       "          .style('opacity', 0.5).each( function() {\n",
       "            this.remove(); BrunelD3.removeLabels(this); \n",
       "        });\n",
       "      }\n",
       "\n",
       "      return {\n",
       "        data:           function() { return processed },\n",
       "        original:       function() { return original },\n",
       "        internal:       function() { return data },\n",
       "        selection:      function() { return merged },\n",
       "        makeData:       makeData,\n",
       "        build:          build,\n",
       "        chart:          function() { return charts[1] },\n",
       "        group:          function() { return elementGroup },\n",
       "        fields: {\n",
       "          y:            ['total_time'],\n",
       "          key:          ['#row'],\n",
       "          color:        ['product_line'],\n",
       "          opacity:      ['#selection']\n",
       "        }\n",
       "      };\n",
       "    }();\n",
       "\n",
       "    function build(time, noData) {\n",
       "      var first = elements[0].data() == null;\n",
       "      if (first) time = 0;                                           // no transition for first call\n",
       "      if ((first || time > -1) && !noData) {\n",
       "        elements[0].makeData();\n",
       "        BrunelD3.addLegend(legends, legends._legend);\n",
       "      }\n",
       "      elements[0].build(time);\n",
       "    }\n",
       "\n",
       "    // Expose the following components of the chart\n",
       "    return {\n",
       "      elements : elements,\n",
       "      interior : interior,\n",
       "      scales: {x:scale_x, y:scale_y},\n",
       "      zoom: function(params, time) {\n",
       "          if (params) zoom.on('zoom').call(zoomNode, params, time);\n",
       "          return d3.zoomTransform(zoomNode);\n",
       "      },\n",
       "      build : build\n",
       "    };\n",
       "    }();\n",
       "\n",
       "  function setData(rowData, i) { datasets[i||0] = BrunelD3.makeData(rowData) }\n",
       "  function updateAll(time) { charts.forEach(function(x) {x.build(time || 0)}) }\n",
       "  function buildAll() {\n",
       "    for (var i=0;i<arguments.length;i++) setData(arguments[i], i);\n",
       "    updateAll(transitionTime);\n",
       "  }\n",
       "\n",
       "  return {\n",
       "    dataPreProcess:     function(f) { if (f) pre = f; return pre },\n",
       "    dataPostProcess:    function(f) { if (f) post = f; return post },\n",
       "    data:               function(d,i) { if (d) setData(d,i); return datasets[i||0] },\n",
       "    visId:              visId,\n",
       "    build:              buildAll,\n",
       "    rebuild:            updateAll,\n",
       "    charts:             charts\n",
       "  }\n",
       "}\n",
       "\n",
       "// Data Tables /////////////////////////////////////////////////////////////////////////////////////\n",
       "\n",
       "var table1 = {\n",
       "   summarized: false,\n",
       "   names: ['day', 'page_hits', 'product_line', 'total_time', 'product', 'dayofweek'], \n",
       "   options: ['numeric', 'numeric', 'string', 'numeric', 'string', 'string'], \n",
       "   rows: [[16, 1, 'headphones', 240, 'A-head', 'Fri'], [16, 1, 'headphones', 240, 'B-head', 'Fri'],\n",
       "  [16, 1, 'smartphones', 180, 'M-phone', 'Fri'], [16, 1, 'smartphones', 120, 'S-phone', 'Fri'],\n",
       "  [16, 1, 'smartphones', 60, 'S-phone', 'Fri'], [16, 1, 'smartphones', 60, 'S-phone', 'Fri'],\n",
       "  [16, 1, 'smartphones', 180, 'S-phone', 'Fri'], [17, 1, 'computers', 120, 'A-top', 'Sat'],\n",
       "  [17, 1, 'computers', 120, 'L-top', 'Sat'], [17, 1, 'computers', 180, 'H-top', 'Sat'],\n",
       "  [17, 1, 'computers', 180, 'D-top', 'Sat'], [17, 1, 'smartphones', 60, 'S-phone', 'Sat'],\n",
       "  [17, 1, 'smartphones', 60, 'S-phone', 'Sat'], [21, 1, 'videogames', 120, 'W-game', 'Wed'],\n",
       "  [21, 2, 'smartphones', 120, 'A-phone', 'Wed'], [21, 1, 'smartphones', 180, 'A-phone', 'Wed'],\n",
       "  [21, 1, 'smartphones', 120, 'S-phone', 'Wed'], [21, 1, 'smartphones', 60, 'S-phone', 'Wed'],\n",
       "  [21, 1, 'smartphones', 120, 'M-phone', 'Wed']]\n",
       "};\n",
       "\n",
       "// Call Code to Build the system ///////////////////////////////////////////////////////////////////\n",
       "\n",
       "var v = new BrunelVis('visid6a894250-69c7-4e00-b12d-dbf9d3f0c2e8');\n",
       "v .build(table1);\n",
       "\n",
       "            \"\"\n",
       "        });\n",
       "        });\n",
       "        </script>"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "%%brunel data('weekMetricsDF') \n",
    "title(\"David's Browsing by Day\")\n",
    "x(day) y(page_hits)\n",
    "stack bar\n",
    "  sum(page_hits) color(product_line) tooltip(#all)\n",
    "  axes(x:7,y:'page views') legends(none) interaction(select)|\n",
    "stack polar bar\n",
    "  y(total_time) color(product_line) label(product)\n",
    "  tooltip(\"<b>day of week: \", dayofweek,\n",
    "          \"<p><u>day of month: \", day,\n",
    "          \"</u></b><p><i>product line: \", product_line,\n",
    "          \"</i><p>product: \", product)\n",
    "  opacity(#selection)\n",
    ":: width=1000, height=300"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "## Insight summary from clickstream analysis\n",
    "\n",
    "1. Aggregated web metrics of recent months highlights significant interest in smart phones with A-phones leading the pack. \n",
    "2. User 'David' is a repeat visitor and has explored smart phones multiple times in recent days along with computers and headphones. \n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Apache Toree - Scala",
   "language": "scala",
   "name": "apache_toree_scala"
  },
  "language_info": {
   "file_extension": ".scala",
   "name": "scala",
   "version": "2.11.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
